first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:27:56 +03:00
commit d9f1ea341e
1021 changed files with 70645 additions and 0 deletions

68
.dockerignore Normal file
View File

@@ -0,0 +1,68 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
*.egg
*.pyc
*.pyo
*.pyd
# Virtual Environment
venv/
env/
ENV/
.venv
# Django
*.log
db.sqlite3
db.sqlite3-journal
/staticfiles/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Git
.git/
.gitignore
# Docker
.dockerignore
Dockerfile
docker-compose*.yml
# Documentation
*.md
!README.md
# Test
.coverage
htmlcov/
.pytest_cache/
.tox/
# Environment variables
.env
.env.*
!.env.example
# Secrets
*.pem
*.key
client_secret*.json
# OS
Thumbs.db
# Backup files
*.bak
*~

36
.env Normal file
View File

@@ -0,0 +1,36 @@
# Django Settings
DEBUG=1
SECRET_KEY=your-secret-key-here-change-this-in-production
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 api.denizogur.com.tr
CELERY_BROKER_URL=redis://default:gg7678290@10.80.80.70:6379/10
# 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
# Database Settings (Mevcut PostgreSQL sunucunuz)
USE_POSTGRES=True
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=10.80.80.70
EMAIL_PORT=1025
EMAIL_HOST_USER=''
EMAIL_HOST_PASSWORD=''
EMAIL_USE_TLS=False
EMAIL_USE_SSL=False
DEFAULT_FROM_EMAIL='noreply@localhost'

31
.env.example Normal file
View File

@@ -0,0 +1,31 @@
# Django Settings
DEBUG=0
SECRET_KEY=your-secret-key-here-change-this-in-production
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 yourdomain.com
# Database Settings (Mevcut PostgreSQL sunucunuz)
USE_POSTGRES=True
POSTGRES_DB=server_dj
POSTGRES_USER=server_dj
POSTGRES_PASSWORD=1234
POSTGRES_HOST=10.80.80.50
POSTGRES_PORT=5432
# Celery Settings
CELERY_BROKER_URL=redis://default:your-redis-password@your-redis-host:6379/5
CELERY_RESULT_BACKEND=django-db
# 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_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-email-password
EMAIL_USE_TLS=True

0
.gitignore vendored Normal file
View File

904
AUTH.md Normal file
View File

@@ -0,0 +1,904 @@
# Authentication API Documentation
Bu doküman, Django REST API'nin authentication endpoint'lerini ve kullanım örneklerini içerir.
## 📋 İçindekiler
1. [Genel Bilgiler](#genel-bilgiler)
2. [Registration (Kayıt)](#registration-kayıt)
3. [Email Activation (Aktivasyon)](#email-activation-aktivasyon)
4. [Login (Giriş)](#login-giriş)
5. [Token Refresh](#token-refresh)
6. [Social Authentication](#social-authentication)
7. [User Profile](#user-profile)
8. [Password Reset](#password-reset)
9. [Frontend Entegrasyonu](#frontend-entegrasyonu)
10. [Error Handling](#error-handling)
---
## Genel Bilgiler
**Base URL:** `http://localhost:8000/api/v1/`
**Authentication:** JWT Bearer Token
```
Authorization: Bearer <access_token>
```
**Content-Type:** `application/json`
### Rate Limiting
- **Anonymous users:** 100 requests/hour
- **Authenticated users:** 1000 requests/hour
---
## Registration (Kayıt)
### Endpoint
```
POST /api/v1/auth/users/
```
### Request Body
```json
{
"email": "user@example.com",
"password": "StrongP@ssw0rd123",
"re_password": "StrongP@ssw0rd123",
"first_name": "Ali",
"last_name": "Veli"
}
```
### Response (201 Created)
```json
{
"id": 1,
"email": "user@example.com",
"first_name": "Ali",
"last_name": "Veli"
}
```
### Önemli Notlar
- Kullanıcı oluşturulur ancak **`is_active=False`** olarak ayarlanır
- Aktivasyon emaili otomatik gönderilir
- Kullanıcı email aktivasyonu yapmadan login olamaz
- Password minimum 8 karakter olmalı ve güçlü olmalı
### Curl Example
```bash
curl -X POST http://localhost:8000/api/v1/auth/users/ \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "StrongP@ssw0rd123",
"re_password": "StrongP@ssw0rd123",
"first_name": "Ali",
"last_name": "Veli"
}'
```
---
## Email Activation (Aktivasyon)
### Endpoint
```
POST /api/v1/auth/users/activation/
```
### Request Body
```json
{
"uid": "MQ",
"token": "c4h7vu-a8f3d2e1c4b5a6d7e8f9g0h1"
}
```
### Response (204 No Content)
Başarılı aktivasyon sonrası response body boş döner.
### Önemli Notlar
- `uid` ve `token` aktivasyon emailindeki linkten alınır
- Token 24 saat geçerlidir
- Başarılı aktivasyon sonrası `is_active=True` olur
- Kullanıcı artık login olabilir
### Email Link Format
```
http://localhost:3000/auth/activate/{uid}/{token}/
```
Frontend bu linki yakalayıp backend'e POST request yapmalı.
### Curl Example
```bash
curl -X POST http://localhost:8000/api/v1/auth/users/activation/ \
-H "Content-Type: application/json" \
-d '{
"uid": "MQ",
"token": "c4h7vu-a8f3d2e1c4b5a6d7e8f9g0h1"
}'
```
### Resend Activation Email
```
POST /api/v1/auth/users/resend_activation/
```
Request Body:
```json
{
"email": "user@example.com"
}
```
---
## Login (Giriş)
### Endpoint
```
POST /api/v1/auth/jwt/create/
```
### Request Body
```json
{
"email": "user@example.com",
"password": "StrongP@ssw0rd123"
}
```
### Response (200 OK)
```json
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### Token Bilgileri
- **Access Token:** 60 dakika geçerli
- **Refresh Token:** 7 gün geçerli
- Token rotation aktif (refresh kullanıldığında yeni refresh token döner)
### Önemli Notlar
- Kullanıcı `is_active=False` ise login başarısız olur
- Hatalı email/password için 401 Unauthorized döner
### Curl Example
```bash
curl -X POST http://localhost:8000/api/v1/auth/jwt/create/ \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "StrongP@ssw0rd123"
}'
```
### Error Response (401 Unauthorized)
```json
{
"detail": "No active account found with the given credentials"
}
```
---
## Token Refresh
### Endpoint
```
POST /api/v1/auth/jwt/refresh/
```
### Request Body
```json
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### Response (200 OK)
```json
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### Önemli Notlar
- Yeni access token ve yeni refresh token döner (rotation)
- Eski refresh token blacklist'e eklenir
- Refresh token expire olduysa 401 döner
### Curl Example
```bash
curl -X POST http://localhost:8000/api/v1/auth/jwt/refresh/ \
-H "Content-Type: application/json" \
-d '{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
```
---
## Social Authentication
### Supported Providers
- **Google:** `google-oauth2`
- **GitHub:** `github`
- **Facebook:** `facebook`
### Endpoint
```
POST /api/v1/auth/social/<provider>/
```
### Request Body
```json
{
"access_token": "ya29.a0AfH6SMBx..."
}
```
### Response (200 OK)
```json
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"email": "user@example.com",
"first_name": "Ali",
"last_name": "Veli",
"is_active": true,
"date_joined": "2025-12-12T21:30:00Z"
}
}
```
### Önemli Notlar
- Social login ile gelen kullanıcılar **otomatik aktif** (`is_active=True`)
- Email aktivasyon gerekmez
- Kullanıcı yoksa otomatik oluşturulur
- Provider'dan email alınamazsa hata döner
### Google OAuth2 Example
#### 1. Frontend'de Google OAuth
```javascript
// Google OAuth2 ile token al
const googleUser = await gapi.auth2.getAuthInstance().signIn();
const accessToken = googleUser.getAuthResponse().access_token;
// Backend'e gönder
const response = await fetch('http://localhost:8000/api/v1/auth/social/google-oauth2/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
access_token: accessToken
})
});
const data = await response.json();
// data.access, data.refresh, data.user
```
#### 2. Curl Example
```bash
curl -X POST http://localhost:8000/api/v1/auth/social/google-oauth2/ \
-H "Content-Type: application/json" \
-d '{
"access_token": "ya29.a0AfH6SMBx..."
}'
```
### GitHub OAuth2 Example
```bash
curl -X POST http://localhost:8000/api/v1/auth/social/github/ \
-H "Content-Type: application/json" \
-d '{
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a"
}'
```
### Error Responses
**Invalid Provider (400)**
```json
{
"error": "Invalid provider. Must be one of: google-oauth2, github, facebook"
}
```
**Missing Token (400)**
```json
{
"error": "access_token is required"
}
```
**Authentication Failed (401)**
```json
{
"error": "Authentication failed. Invalid token."
}
```
**Email Not Provided (403)**
```json
{
"error": "Authentication forbidden. Email not provided by provider or permission denied."
}
```
---
## User Profile
### Get Current User
```
GET /api/v1/auth/users/me/
```
**Headers:**
```
Authorization: Bearer <access_token>
```
**Response (200 OK):**
```json
{
"id": 1,
"email": "user@example.com",
"first_name": "Ali",
"last_name": "Veli",
"is_active": true,
"date_joined": "2025-12-12T21:30:00Z"
}
```
### Update Current User
```
PATCH /api/v1/auth/users/me/
```
**Request Body:**
```json
{
"first_name": "Ahmet",
"last_name": "Yılmaz"
}
```
### Curl Example
```bash
curl -X GET http://localhost:8000/api/v1/auth/users/me/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
---
## Password Reset
### 1. Request Password Reset
```
POST /api/v1/auth/users/reset_password/
```
**Request Body:**
```json
{
"email": "user@example.com"
}
```
**Response (204 No Content)**
Email gönderilir, link formatı:
```
http://localhost:3000/auth/password/reset/confirm/{uid}/{token}/
```
### 2. Confirm Password Reset
```
POST /api/v1/auth/users/reset_password_confirm/
```
**Request Body:**
```json
{
"uid": "MQ",
"token": "c4h7vu-a8f3d2e1c4b5a6d7e8f9g0h1",
"new_password": "NewStrongP@ssw0rd123",
"re_new_password": "NewStrongP@ssw0rd123"
}
```
**Response (204 No Content)**
---
## Frontend Entegrasyonu
### Nuxt.js 3 Example
#### 1. Composable: `useAuth.ts`
```typescript
// composables/useAuth.ts
export const useAuth = () => {
const config = useRuntimeConfig();
const accessToken = useCookie('access_token');
const refreshToken = useCookie('refresh_token');
const register = async (userData: {
email: string;
password: string;
re_password: string;
first_name: string;
last_name: string;
}) => {
const { data, error } = await useFetch(`${config.public.apiBase}/auth/users/`, {
method: 'POST',
body: userData,
});
return { data, error };
};
const login = async (email: string, password: string) => {
const { data, error } = await useFetch(`${config.public.apiBase}/auth/jwt/create/`, {
method: 'POST',
body: { email, password },
});
if (data.value) {
accessToken.value = data.value.access;
refreshToken.value = data.value.refresh;
}
return { data, error };
};
const socialLogin = async (provider: string, accessTokenValue: string) => {
const { data, error } = await useFetch(
`${config.public.apiBase}/auth/social/${provider}/`,
{
method: 'POST',
body: { access_token: accessTokenValue },
}
);
if (data.value) {
accessToken.value = data.value.access;
refreshToken.value = data.value.refresh;
}
return { data, error };
};
const getUser = async () => {
if (!accessToken.value) return null;
const { data } = await useFetch(`${config.public.apiBase}/auth/users/me/`, {
headers: {
Authorization: `Bearer ${accessToken.value}`,
},
});
return data.value;
};
const logout = () => {
accessToken.value = null;
refreshToken.value = null;
};
return {
register,
login,
socialLogin,
getUser,
logout,
accessToken,
refreshToken,
};
};
```
#### 2. Register Page: `pages/auth/register.vue`
```vue
<template>
<div>
<h1>Register</h1>
<form @submit.prevent="handleRegister">
<input v-model="form.email" type="email" placeholder="Email" required />
<input v-model="form.first_name" placeholder="First Name" />
<input v-model="form.last_name" placeholder="Last Name" />
<input v-model="form.password" type="password" placeholder="Password" required />
<input v-model="form.re_password" type="password" placeholder="Confirm Password" required />
<button type="submit">Register</button>
</form>
<p v-if="message">{{ message }}</p>
</div>
</template>
<script setup lang="ts">
const { register } = useAuth();
const form = ref({
email: '',
password: '',
re_password: '',
first_name: '',
last_name: '',
});
const message = ref('');
const handleRegister = async () => {
const { data, error } = await register(form.value);
if (error.value) {
message.value = 'Registration failed';
} else {
message.value = 'Registration successful! Please check your email to activate your account.';
}
};
</script>
```
#### 3. Activation Page: `pages/auth/activate/[uid]/[token].vue`
```vue
<template>
<div>
<h1>Account Activation</h1>
<p v-if="loading">Activating your account...</p>
<p v-else-if="success"> Account activated successfully! You can now login.</p>
<p v-else-if="error"> Activation failed. Link may be expired.</p>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const config = useRuntimeConfig();
const loading = ref(true);
const success = ref(false);
const error = ref(false);
onMounted(async () => {
const { uid, token } = route.params;
try {
await $fetch(`${config.public.apiBase}/auth/users/activation/`, {
method: 'POST',
body: { uid, token },
});
success.value = true;
} catch (e) {
error.value = true;
} finally {
loading.value = false;
}
});
</script>
```
#### 4. Login Page: `pages/auth/login.vue`
```vue
<template>
<div>
<h1>Login</h1>
<form @submit.prevent="handleLogin">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
<div class="social-login">
<button @click="handleGoogleLogin">Login with Google</button>
<button @click="handleGithubLogin">Login with GitHub</button>
</div>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup lang="ts">
const { login, socialLogin } = useAuth();
const router = useRouter();
const email = ref('');
const password = ref('');
const error = ref('');
const handleLogin = async () => {
const { data, error: loginError } = await login(email.value, password.value);
if (loginError.value) {
error.value = 'Login failed. Please check your credentials.';
} else {
router.push('/dashboard');
}
};
const handleGoogleLogin = async () => {
// Google OAuth2 implementation
// Use @nuxtjs/google-oauth2 or similar
const googleToken = await getGoogleAccessToken(); // Your implementation
const { data, error: socialError } = await socialLogin('google-oauth2', googleToken);
if (!socialError.value) {
router.push('/dashboard');
}
};
const handleGithubLogin = async () => {
// GitHub OAuth2 implementation
const githubToken = await getGithubAccessToken(); // Your implementation
const { data, error: socialError } = await socialLogin('github', githubToken);
if (!socialError.value) {
router.push('/dashboard');
}
};
</script>
```
### Next.js 14 Example
#### 1. Auth Context: `context/AuthContext.tsx`
```typescript
'use client';
import { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: number;
email: string;
first_name: string;
last_name: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
register: (userData: any) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
useEffect(() => {
// Load token from localStorage
const token = localStorage.getItem('access_token');
if (token) {
setAccessToken(token);
fetchUser(token);
}
}, []);
const fetchUser = async (token: string) => {
try {
const response = await fetch('http://localhost:8000/api/v1/auth/users/me/', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
const data = await response.json();
setUser(data);
} catch (error) {
console.error('Failed to fetch user', error);
}
};
const login = async (email: string, password: string) => {
const response = await fetch('http://localhost:8000/api/v1/auth/jwt/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
setAccessToken(data.access);
await fetchUser(data.access);
};
const logout = () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
setAccessToken(null);
setUser(null);
};
const register = async (userData: any) => {
const response = await fetch('http://localhost:8000/api/v1/auth/users/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Registration failed');
}
};
return (
<AuthContext.Provider value={{ user, login, logout, register }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
```
---
## Error Handling
### Common Error Codes
| Status Code | Meaning | Common Causes |
|-------------|---------|---------------|
| 400 | Bad Request | Invalid data, validation errors |
| 401 | Unauthorized | Invalid credentials, expired token |
| 403 | Forbidden | Account not activated, permission denied |
| 404 | Not Found | Endpoint doesn't exist |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side error |
### Error Response Format
```json
{
"detail": "Error message here",
"field_name": ["Field-specific error"]
}
```
### Example: Registration Validation Error
```json
{
"email": ["A user with that email already exists."],
"password": ["This password is too common."]
}
```
---
## Testing with Postman/Insomnia
### 1. Register
```
POST http://localhost:8000/api/v1/auth/users/
Content-Type: application/json
{
"email": "test@example.com",
"password": "TestP@ssw0rd123",
"re_password": "TestP@ssw0rd123",
"first_name": "Test",
"last_name": "User"
}
```
### 2. Check Email (MailPit)
Open: `http://localhost:8025`
### 3. Activate Account
```
POST http://localhost:8000/api/v1/auth/users/activation/
Content-Type: application/json
{
"uid": "MQ",
"token": "c4h7vu-a8f3d2e1c4b5a6d7e8f9g0h1"
}
```
### 4. Login
```
POST http://localhost:8000/api/v1/auth/jwt/create/
Content-Type: application/json
{
"email": "test@example.com",
"password": "TestP@ssw0rd123"
}
```
### 5. Get User Profile
```
GET http://localhost:8000/api/v1/auth/users/me/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
---
## Environment Variables
### Development (.env.dev)
```bash
DEBUG=True
SECRET_KEY=your-secret-key-here
ALLOWED_HOSTS=localhost,127.0.0.1
# Database
DATABASE_URL=sqlite:///db.sqlite3
# Email (MailPit)
EMAIL_HOST=localhost
EMAIL_PORT=1025
EMAIL_USE_TLS=False
# CORS
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
```
### Production (.env.prod)
```bash
DEBUG=False
SECRET_KEY=your-production-secret-key
ALLOWED_HOSTS=yourdomain.com,api.yourdomain.com
# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname
# Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
# Social Auth
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-google-client-id
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-google-client-secret
SOCIAL_AUTH_GITHUB_KEY=your-github-client-id
SOCIAL_AUTH_GITHUB_SECRET=your-github-client-secret
# CORS
CORS_ALLOWED_ORIGINS=https://yourdomain.com
```
---
## Support
Sorularınız için:
- GitHub Issues: [Your Repo]
- Email: support@yourdomain.com
- Documentation: [Your Docs URL]
---
**Last Updated:** 2025-12-12
**Version:** 1.0.0

110
CACHE.md Normal file
View File

@@ -0,0 +1,110 @@
# Cache Dokumani
Bu dokuman, `home`, `portfolio` ve `settings` app'lerinde yapilan cache
ayarlarini ve otomatik cache temizleme (invalidation) akislarini aciklar.
## Genel
- Cache suresi: 5 dakika (`60 * 5` saniye).
- Cache Django cache backend uzerinden tutulur (`django.core.cache.cache`).
- Veri ekleme, silme ve guncelleme (post_save/post_delete) veya M2M degisimi
oldugunda ilgili cache temizlenir.
## Home App
### Cache Key'leri
- `home:active_home`
- `home:active_aboutme`
- `home:service_list`
- `home:service_title`
- `home:resume`
- `home:education_list`
- `home:experience_list`
- `home:skill_list`
- `home:knowledge_list`
- `home:active_menu`
### Cache Kullanan Endpoint'ler
- `HomeDetailView` -> `home:active_home`
- `AboutMeDetailView` -> `home:active_aboutme`
- `MyServiceList` -> `home:service_list`
- `MyServiceTitleDetailView` -> `home:service_title`
- `MyResumeDetailView` -> `home:resume`
- `EducationListView` -> `home:education_list`
- `ExperienceListView` -> `home:experience_list`
- `SkillListView` -> `home:skill_list`
- `KnowledgeListView` -> `home:knowledge_list`
- `MainMenuDetailView` -> `home:active_menu`
### Cache Temizleme
`home/signals.py` uzerinden asagidaki modellerde degisim oldugunda
tum `home:*` cache'leri temizlenir:
- `Home`
- `AboutMe`
- `MyService`
- `MyServiceTitle`
- `MyResume`
- `Education`
- `Experience`
- `Skill`
- `Knowledge`
- `MainMenu`
Ek olarak `Home.tags` M2M degisimi (`post_add`, `post_remove`, `post_clear`)
oldugunda cache temizlenir.
## Portfolio App
### Cache Key'leri
- `portfolio:category_list`
- `portfolio:category_detail:<slug>`
- `portfolio:portfolio_list`
- `portfolio:portfolio_detail:<pk>`
### Cache Kullanan Endpoint'ler
- `CategoryList` -> `portfolio:category_list`
- `CategoryDetail` -> `portfolio:category_detail:<slug>`
- `PortfolioList` -> `portfolio:portfolio_list`
- `PortfolioDetail` -> `portfolio:portfolio_detail:<pk>`
### Cache Temizleme
`portfolio/signals.py` uzerinden asagidaki durumlarda cache temizlenir:
- `Category` kaydi kayit/silme/guncelleme -> kategori list ve ilgili detay
- `Portfolio` kaydi kayit/silme/guncelleme -> portfolio list ve ilgili detay
- `Portfolio.categories` M2M degisimi -> ilgili portfolio detay ve list
## Settings App
### Cache Key'leri
- `settings:detail`
- `settings:site_status`
### Cache Kullanan Endpoint'ler
- `SettingDetailView` -> `settings:detail`
- `SettingOpenCloseDetailView` -> `settings:site_status`
### Cache Temizleme
`settings/signals.py` uzerinden asagidaki modellerde degisim oldugunda
tum cache temizlenir:
- `Setting`
- `SiteSettings`
## Notlar
- Signal'larin calismasi icin `apps.py` icindeki `ready()` metodlari
ile signal moduleri yuklenir:
- `home/apps.py`
- `portfolio/apps.py`
- `settings/apps.py`

208
CONTACT_EMAIL_SETUP.md Normal file
View File

@@ -0,0 +1,208 @@
# Contact Form - Email Gönderimi (Celery)
## Yapılan Değişiklikler
### 1. Contact Model Güncellemeleri
- `user` alanı artık `null=True, blank=True` (anonim kullanıcılar da form gönderebilir)
- `ip` alanı artık `null=True, blank=True` (opsiyonel)
- `on_delete=SET_NULL` olarak değiştirildi
### 2. Yeni Dosyalar
- **contact/tasks.py**: Celery task ile email gönderimi
- **core/celery.py**: Celery yapılandırması
- **core/__init__.py**: Celery app otomatik yükleme
- **test_contact_api.py**: API test scripti
### 3. View Güncellemeleri (contact/views.py)
- `permission_classes = [AllowAny]` - Herkes form gönderebilir
- IP adresi otomatik olarak algılanır
- Kullanıcı giriş yapmışsa user kaydedilir, yoksa None
- Contact kaydedildikten sonra Celery task ile email gönderilir
### 4. Serializer Güncellemeleri
- Gereksiz alanlar kaldırıldı (ip, updated_at)
- Sadece gerekli alanlar kabul edilir
## Kurulum ve Çalıştırma
### 1. Migration Uygula (TAMAMLANDI ✅)
```bash
python manage.py makemigrations contact
python manage.py migrate contact
```
### 2. Redis'in Çalıştığından Emin Olun
Celery için Redis broker gerekli. Settings'te tanımlı:
```
CELERY_BROKER_URL = redis://default:1923btO**@10.80.80.70:6379/5
```
### 3. MailPit'in Çalıştığından Emin Olun
Email testleri için MailPit kullanılıyor:
- SMTP: localhost:1025
- Web UI: http://localhost:8025
MailPit başlatma (Docker):
```bash
docker run -d -p 1025:1025 -p 8025:8025 --name mailpit axllent/mailpit
```
### 4. Celery Worker Başlat
Yeni bir terminal açın ve şu komutu çalıştırın:
```bash
cd /Users/beyhan/Desktop/Projeler/Python/atabackend
source venv/bin/activate
celery -A core worker --loglevel=info
```
### 5. Django Sunucusunu Başlat
Başka bir terminal açın:
```bash
cd /Users/beyhan/Desktop/Projeler/Python/atabackend
source venv/bin/activate
python manage.py runserver
```
### 6. Test Et
```bash
# Terminal'de test script'i çalıştır
python test_contact_api.py
# Veya cURL ile
curl -X POST http://127.0.0.1:8000/api/v1/contact/create/ \
-H "Content-Type: application/json" \
-d '{
"name": "Test Kullanıcı",
"email": "test@example.com",
"subject": "Test Konusu",
"message": "Bu bir test mesajıdır."
}'
```
### 7. Email Kontrolü
- MailPit web arayüzüne gidin: http://localhost:8025
- Gönderilen emaili göreceksiniz
## API Endpoint
**URL**: `POST /api/v1/contact/create/`
**Permission**: AllowAny (Kimlik doğrulama gerekmez)
**Request Body**:
```json
{
"name": "Adınız Soyadınız",
"email": "email@example.com",
"subject": "Konu",
"message": "Mesajınız"
}
```
**Response** (201 Created):
```json
{
"id": 1,
"name": "Adınız Soyadınız",
"email": "email@example.com",
"subject": "Konu",
"message": "Mesajınız",
"created_at": "2026-01-15T10:30:00Z"
}
```
## Celery Task İzleme
### Task Logları
Celery worker terminalinde task durumunu görebilirsiniz:
```
[2026-01-15 10:30:00,123: INFO/MainProcess] Task contact.tasks.send_contact_email[...] received
[2026-01-15 10:30:00,456: INFO/ForkPoolWorker-1] Task contact.tasks.send_contact_email[...] succeeded
```
### Django Admin'den İzleme
Celery Beat ve Results kurulu, admin panelden task sonuçlarını görebilirsiniz:
- http://127.0.0.1:8000/admin/django_celery_results/
## Email Şablonu
Gönderilen email formatı:
```
Konu: Yeni İletişim Mesajı: [KONU]
Yeni bir iletişim mesajı alındı!
Gönderen: [AD SOYAD]
Email: [EMAIL]
IP Adresi: [IP]
Konu: [KONU]
Mesaj:
[MESAJ İÇERİĞİ]
---
Bu mesaj otomatik olarak gönderilmiştir.
```
## Production Ayarları
Production'da şunları yapın:
1. **Email Backend'i değiştir** (settings.py):
```python
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com' # veya kendi SMTP sunucunuz
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
DEFAULT_FROM_EMAIL = 'noreply@denizogur.com.tr'
```
2. **Alıcı email adresini değiştir** (contact/tasks.py):
```python
recipient_list=['info@denizogur.com.tr'] # Production email
```
3. **Redis production URL'i kullan**
4. **Celery worker'ı production'da service olarak çalıştır**
## Sorun Giderme
### Email Gönderilmiyor
1. Celery worker çalışıyor mu? → Terminal'i kontrol edin
2. Redis bağlantısı var mı? → `CELERY_BROKER_URL` kontrol edin
3. MailPit çalışıyor mu? → http://localhost:8025 kontrol edin
### Task Çalışmıyor
```bash
# Celery task'ı manuel test et
python manage.py shell
>>> from contact.tasks import send_contact_email
>>> send_contact_email.delay('Test', 'test@test.com', 'Konu', 'Mesaj', '127.0.0.1')
```
### Celery Worker Hatası
```bash
# Celery worker'ı yeniden başlat
# Worker terminalinde Ctrl+C
# Sonra tekrar başlat:
celery -A core worker --loglevel=info
```
## İleride Eklenebilecek Özellikler
1. **Email Template (HTML)**: Rich HTML email şablonu
2. **Auto-reply**: Kullanıcıya otomatik teşekkür emaili
3. **Rate Limiting**: Spam koruması için rate limiting
4. **File Upload**: Contact formuna dosya ekleme
5. **Admin Notifications**: Yeni mesaj geldiğinde admin bildirim
6. **Email Queue Retry**: Email başarısız olursa yeniden deneme
## Referanslar
- Celery Docs: https://docs.celeryq.dev/
- Django Celery Results: https://django-celery-results.readthedocs.io/
- MailPit: https://github.com/axllent/mailpit

194
COPILOT_MEMORY.md Normal file
View File

@@ -0,0 +1,194 @@
# Copilot Memory - Django Auth System Development
Bu dosya, Django 6.0 projemizde Custom User + Djoser + JWT + Social Auth sisteminin geliştirilme sürecini takip eder.
---
## 2025-12-12T21:35:00Z
### ✅ Değişiklik Özeti: İlk Kurulum - Custom User Model ve Auth Sistemi Temeli
**Tamamlanan İşler:**
1. **Custom User Model Oluşturuldu** (`accounts/models.py`)
- `CustomUser` modeli: Email tabanlı authentication (username yok)
- `CustomUserManager`: `create_user` ve `create_superuser` metodları
- Alanlar: `email` (unique), `first_name`, `last_name`, `is_staff`, `is_active`, `date_joined`
- `USERNAME_FIELD = "email"`
2. **Admin Panel Konfigürasyonu** (`accounts/admin.py`)
- `CustomUserAdmin` sınıfı ile Django admin'de custom user yönetimi
- List display, filters, search fields yapılandırıldı
3. **Serializers Oluşturuldu** (`accounts/serializers.py`)
- `CustomUserCreateSerializer`: Register için, `is_active=False` set eder
- `CustomUserSerializer`: User profil bilgileri için
- `SocialLoginSerializer`: Social auth için provider + access_token
4. **Social Auth Pipeline** (`accounts/pipeline.py`)
- `activate_user` fonksiyonu: Social login ile gelen kullanıcıları otomatik aktif eder
- Normal register: `is_active=False` (email aktivasyon gerekli)
- Social register: `is_active=True` (direkt aktif)
5. **Social Login View** (`accounts/views.py`)
- `SocialLoginView`: Provider token'ı doğrular, user oluşturur/bulur, JWT döner
- Desteklenen provider'lar: google-oauth2, github, facebook
- Error handling: AuthForbidden, AuthException, genel hatalar
6. **Settings.py Tam Konfigürasyonu** (`core/settings.py`)
- `AUTH_USER_MODEL = 'accounts.CustomUser'`
- `INSTALLED_APPS`: rest_framework, rest_framework_simplejwt, djoser, corsheaders, social_django, accounts
- **REST_FRAMEWORK**: JWT authentication, throttling (100/hour anon, 1000/hour user)
- **SIMPLE_JWT**: 60 min access, 7 days refresh, token rotation, blacklist
- **DJOSER**: Email activation, custom serializers, password reset
- **EMAIL**: MailPit (localhost:1025) dev için, production için SMTP placeholder
- **CORS**: localhost:3000, 5173, 8080 (Nuxt/Next/Vue için)
- **SOCIAL_AUTH**: Google, GitHub, Facebook backends + custom pipeline
7. **Email Templates Oluşturuldu** (`templates/email/`)
- `activation_email.html` / `.txt`: Hesap aktivasyon emaili
- `confirmation_email.html` / `.txt`: Aktivasyon başarılı emaili
- `password_reset_email.html` / `.txt`: Şifre sıfırlama emaili
- Modern, responsive HTML tasarım + plain text alternatifi
8. **URL Routing** (`accounts/urls.py`)
- Djoser endpoints: `/api/v1/auth/users/` (register), `/api/v1/auth/users/activation/` (activate)
- JWT endpoints: `/api/v1/auth/jwt/create/` (login), `/api/v1/auth/jwt/refresh/`
- Social auth: `/api/v1/auth/social/<provider>/`
- Python Social Auth URLs: `/api/v1/social/`
9. **Database Migrations**
- `accounts/migrations/0001_initial.py`: CustomUser model
- `social_django` migrations: Social auth tabloları
- Tüm migration'lar başarıyla uygulandı (migrate completed)
### 📁 Değiştirilen/Oluşturulan Dosyalar:
- `accounts/models.py` (yeni)
- `accounts/admin.py` (güncellendi)
- `accounts/serializers.py` (yeni)
- `accounts/pipeline.py` (yeni)
- `accounts/views.py` (güncellendi)
- `accounts/urls.py` (güncellendi)
- `accounts/migrations/0001_initial.py` (oluşturuldu)
- `core/settings.py` (kapsamlı güncelleme)
- `templates/email/activation_email.html` (yeni)
- `templates/email/activation_email.txt` (yeni)
- `templates/email/confirmation_email.html` (yeni)
- `templates/email/confirmation_email.txt` (yeni)
- `templates/email/password_reset_email.html` (yeni)
- `templates/email/password_reset_email.txt` (yeni)
### 🎯 Sistem Özellikleri:
**Authentication Akışları:**
1. **Normal Register (Email/Password):**
```
POST /api/v1/auth/users/
Body: { "email", "password", "re_password", "first_name", "last_name" }
→ User oluşturulur (is_active=False)
→ Aktivasyon emaili gönderilir
→ POST /api/v1/auth/users/activation/ { "uid", "token" }
→ is_active=True olur
→ POST /api/v1/auth/jwt/create/ { "email", "password" }
→ JWT tokens alınır
```
2. **Social Login:**
```
POST /api/v1/auth/social/google-oauth2/
Body: { "access_token": "..." }
→ Provider'dan user bilgisi alınır
→ User bulunur/oluşturulur (is_active=True)
→ JWT tokens direkt döner
```
3. **Login:**
```
POST /api/v1/auth/jwt/create/
Body: { "email", "password" }
→ Access + Refresh token döner
```
4. **Token Refresh:**
```
POST /api/v1/auth/jwt/refresh/
Body: { "refresh": "..." }
→ Yeni access token döner
```
### ⚙️ Yapılandırma Gereksinimleri:
**Environment Variables (Production için):**
```bash
# Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=noreply@yourdomain.com
# Social Auth - Google
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-google-client-id
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-google-client-secret
# Social Auth - GitHub
SOCIAL_AUTH_GITHUB_KEY=your-github-client-id
SOCIAL_AUTH_GITHUB_SECRET=your-github-client-secret
# Social Auth - Facebook
SOCIAL_AUTH_FACEBOOK_KEY=your-facebook-app-id
SOCIAL_AUTH_FACEBOOK_SECRET=your-facebook-app-secret
```
**Development Tools:**
- MailPit: `localhost:1025` (SMTP), `localhost:8025` (Web UI)
- Database: SQLite (db.sqlite3)
### 📝 Next Steps:
1. **Testing (Yüksek Öncelik):**
- [ ] Unit tests: Register → is_active=False check
- [ ] Unit tests: Activation → is_active=True check
- [ ] Unit tests: Login → aktif/inaktif user scenarios
- [ ] Unit tests: Social login → user creation + JWT response
- [ ] Integration tests: Full auth flow
2. **Dokümantasyon:**
- [ ] `AUTH.md` oluştur: Tüm endpoint'ler, request/response örnekleri
- [ ] Frontend entegrasyon kılavuzu (Nuxt.js + Next.js)
- [ ] Environment variables dokümantasyonu
- [ ] Deployment checklist
3. **İyileştirmeler:**
- [ ] Rate limiting test et
- [ ] Email template'lerini test et (MailPit ile)
- [ ] Social auth provider'ları test et
- [ ] Error mesajlarını frontend-friendly hale getir
- [ ] Logging ekle (özellikle auth failures için)
4. **Güvenlik:**
- [ ] HTTPS için production settings
- [ ] CSRF token stratejisi netleştir
- [ ] JWT secret key'i environment variable'a taşı
- [ ] Rate limiting değerlerini production için ayarla
5. **Opsiyonel Özellikler:**
- [ ] Email değiştirme flow'u
- [ ] 2FA (Two-Factor Authentication)
- [ ] Remember me functionality
- [ ] Account deletion
- [ ] Social account linking (birden fazla provider)
### 🐛 Bilinen Sorunlar:
- Yok (şu an için)
### 📚 Referanslar:
- Djoser Docs: https://djoser.readthedocs.io/
- SimpleJWT Docs: https://django-rest-framework-simplejwt.readthedocs.io/
- Python Social Auth: https://python-social-auth.readthedocs.io/
- Django REST Framework: https://www.django-rest-framework.org/
---

404
DOCKER_CELERY.md Normal file
View File

@@ -0,0 +1,404 @@
# 🚀 Docker Compose ile Celery Kullanımı
Bu döküman, Docker Compose üzerinde Celery worker ve beat'in nasıl çalıştırılacağınııklar.
## 📋 Genel Bakış
Projede iki Docker Compose yapılandırması bulunmaktadır:
1. **docker-compose.yml** - Development (Geliştirme) ortamı
2. **docker-compose.prod.yml** - Production (Canlı) ortamı
Her iki ortamda da Celery worker ve beat aynı container içinde çalışır:
```bash
celery -A core worker --beat --scheduler django --loglevel=info
```
## 🏗️ Servis Yapısı
### Development (docker-compose.yml)
```yaml
services:
web: # Django uygulaması
celery: # Celery worker + beat
```
### Production (docker-compose.prod.yml)
```yaml
services:
web-atahan: # Django uygulaması (Gunicorn)
celery-atahan: # Celery worker + beat
nginx: # Reverse proxy
```
## 🚀 Başlatma
### Development Ortamı
```bash
# Tüm servisleri başlat (web + celery)
docker-compose up
# Arka planda çalıştır
docker-compose up -d
# Sadece Celery'yi başlat
docker-compose up celery
# Rebuild ile başlat
docker-compose up --build
```
### Production Ortamı
```bash
# Tüm servisleri başlat
docker-compose -f docker-compose.prod.yml up -d
# Sadece Celery'yi başlat
docker-compose -f docker-compose.prod.yml up -d celery-atahan
# Rebuild ile başlat
docker-compose -f docker-compose.prod.yml up --build -d
```
## 📊 Log İzleme
### Development
```bash
# Tüm servislerin logları
docker-compose logs -f
# Sadece Celery logları
docker-compose logs -f celery
# Son 100 satır
docker-compose logs --tail=100 celery
# Belirli bir tarihten sonraki loglar
docker-compose logs --since 2026-01-15T10:00:00 celery
```
### Production
```bash
# Celery logları
docker-compose -f docker-compose.prod.yml logs -f celery-atahan
# Tüm servisler
docker-compose -f docker-compose.prod.yml logs -f
```
## 🔄 Yeniden Başlatma
### Development
```bash
# Celery'yi yeniden başlat
docker-compose restart celery
# Tüm servisleri yeniden başlat
docker-compose restart
```
### Production
```bash
# Celery'yi yeniden başlat
docker-compose -f docker-compose.prod.yml restart celery-atahan
# Tüm servisleri yeniden başlat
docker-compose -f docker-compose.prod.yml restart
```
## 🛑 Durdurma
### Development
```bash
# Servisleri durdur (container'ları kaldır)
docker-compose down
# Sadece Celery'yi durdur
docker-compose stop celery
# Volume'leri de sil
docker-compose down -v
```
### Production
```bash
# Servisleri durdur
docker-compose -f docker-compose.prod.yml down
# Sadece Celery'yi durdur
docker-compose -f docker-compose.prod.yml stop celery-atahan
```
## 🔧 Container'a Bağlanma
### Development
```bash
# Celery container'a bash ile bağlan
docker-compose exec celery bash
# Celery container'da komut çalıştır
docker-compose exec celery ls -la
docker-compose exec celery python manage.py shell
```
### Production
```bash
# Celery container'a bash ile bağlan
docker-compose -f docker-compose.prod.yml exec celery-atahan bash
# Komut çalıştır
docker-compose -f docker-compose.prod.yml exec celery-atahan python manage.py shell
```
## 📝 Celery Task Yönetimi
### Task Sonuçlarını Görüntüleme
```bash
# Django shell aç
docker-compose exec web python manage.py shell
# Shell içinde:
from django_celery_results.models import TaskResult
# Tüm task sonuçları
TaskResult.objects.all()
# Başarılı task'lar
TaskResult.objects.filter(status='SUCCESS')
# Başarısız task'lar
TaskResult.objects.filter(status='FAILURE')
# En son 10 task
TaskResult.objects.order_by('-date_done')[:10]
```
### Django Admin'den Task İzleme
1. Tarayıcıda admin panele girin:
- Development: http://localhost:8000/admin/
- Production: http://your-domain.com/admin/
2. Şu bölümlere gidin:
- **Django Celery Results** → **Task results** - Task sonuçları
- **Django Celery Beat** → **Periodic tasks** - Zamanlanmış task'lar
- **Django Celery Beat** → **Intervals** - Periyodik aralıklar
### Manuel Task Çalıştırma
```bash
# Django shell aç
docker-compose exec web python manage.py shell
# Shell içinde bir task çalıştır:
from contact.tasks import send_contact_email
# Hemen çalıştır (test için)
result = send_contact_email(
name='Test',
email='test@example.com',
subject='Test Subject',
message='Test message',
ip='127.0.0.1'
)
print(result)
# Celery queue'ya ekle (asenkron)
task = send_contact_email.delay(
name='Test',
email='test@example.com',
subject='Test Subject',
message='Test message',
ip='127.0.0.1'
)
print(f"Task ID: {task.id}")
print(f"Task Status: {task.status}")
```
## ⚙️ Environment Variables
### Development (.env veya docker-compose.yml)
```bash
CELERY_BROKER_URL=redis://default:password@host:6379/5
CELERY_RESULT_BACKEND=django-db
```
### Production (.env)
```bash
CELERY_BROKER_URL=redis://default:password@host:6379/5
CELERY_RESULT_BACKEND=django-db
```
## 🐛 Sorun Giderme
### Celery Başlamıyor
```bash
# Logları kontrol et
docker-compose logs celery
# Container durumunu kontrol et
docker-compose ps
# Container'ı yeniden başlat
docker-compose restart celery
# Container'ı rebuild et
docker-compose up --build celery
```
### Redis Bağlantı Hatası
```bash
# Redis bağlantısını test et
docker-compose exec celery python -c "
from celery import Celery
app = Celery('core')
app.config_from_object('django.conf:settings', namespace='CELERY')
print('Connection OK')
"
# Environment variable'ları kontrol et
docker-compose exec celery env | grep CELERY
```
### Task Çalışmıyor
```bash
# Celery worker'ın çalıştığını doğrula
docker-compose logs -f celery
# Task'ın queue'ya eklendiğini kontrol et
docker-compose exec web python manage.py shell
>>> from django_celery_results.models import TaskResult
>>> TaskResult.objects.latest('date_created')
# Task'ı manuel çalıştır
>>> from contact.tasks import send_contact_email
>>> send_contact_email.delay('Test', 'test@test.com', 'Subject', 'Message')
```
### Email Gönderilmiyor
```bash
# Email backend ayarlarını kontrol et
docker-compose exec web python manage.py shell
>>> from django.conf import settings
>>> print(settings.EMAIL_BACKEND)
>>> print(settings.EMAIL_HOST)
>>> print(settings.EMAIL_PORT)
# MailPit kontrol et (development)
# http://localhost:8025
```
## 📈 Performans İzleme
### Container Kaynak Kullanımı
```bash
# Tüm container'ların kaynak kullanımı
docker stats
# Sadece Celery
docker stats django_celery_worker
# Production
docker stats django_celery_prod_atahan
```
### Celery Worker İstatistikleri
```bash
# Celery inspect komutu
docker-compose exec celery celery -A core inspect active
docker-compose exec celery celery -A core inspect stats
docker-compose exec celery celery -A core inspect registered
```
## 🔐 Production Best Practices
### 1. Log Rotation
Production'da log dosyaları büyüyebilir. Docker log rotation kullanın:
```yaml
# docker-compose.prod.yml
celery-atahan:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
```
### 2. Resource Limits
Container'lara kaynak limiti koyun:
```yaml
celery-atahan:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
```
### 3. Health Checks
Celery health check ekleyin:
```yaml
celery-atahan:
healthcheck:
test: ["CMD-SHELL", "celery -A core inspect ping"]
interval: 30s
timeout: 10s
retries: 3
```
### 4. Restart Policy
Otomatik yeniden başlatma:
```yaml
celery-atahan:
restart: unless-stopped
```
## 📚 Ek Kaynaklar
- [Celery Documentation](https://docs.celeryq.dev/)
- [Django Celery Results](https://django-celery-results.readthedocs.io/)
- [Django Celery Beat](https://django-celery-beat.readthedocs.io/)
- [Docker Compose Documentation](https://docs.docker.com/compose/)
## 🆘 Yardım
Sorun yaşıyorsanız:
1. Logları kontrol edin: `docker-compose logs -f celery`
2. Container durumunu kontrol edin: `docker-compose ps`
3. Environment variable'ları kontrol edin: `docker-compose exec celery env`
4. Redis bağlantısını test edin
5. Task'ları manuel çalıştırın ve sonucu gözlemleyin

331
DOCKER_CELERY_QUICKSTART.md Normal file
View File

@@ -0,0 +1,331 @@
# 🐳 Docker Compose ile Celery - Hızlı Başlangıç
Bu döküman, Docker Compose ile Celery'yi hızlıca çalıştırmak için gerekli adımları içerir.
## ✅ Hazırlık
1. **Docker ve Docker Compose yüklü olmalı**
2. **Redis erişimi olmalı** (CELERY_BROKER_URL)
## 🚀 Development Ortamı
### Başlatma
```bash
# Tüm servisleri başlat (Django + Celery)
docker-compose up
# Arka planda çalıştır
docker-compose up -d
# Logları takip et
docker-compose logs -f
# Sadece Celery logları
docker-compose logs -f celery
```
### Test Etme
```bash
# 1. Contact API'yi test et
curl -X POST http://localhost:8000/api/v1/contact/create/ \
-H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "test@example.com",
"subject": "Test Subject",
"message": "This is a test message."
}'
# 2. Celery loglarını kontrol et
docker-compose logs -f celery
# 3. Email'i MailPit'te kontrol et
open http://localhost:8025
```
### Durdurma
```bash
# Servisleri durdur
docker-compose down
# Volume'leri de sil
docker-compose down -v
```
## 🌐 Production Ortamı
### Hazırlık
```bash
# .env dosyası oluştur
cp .env.example .env
# .env dosyasını düzenle
nano .env
# Gerekli değerler:
# - SECRET_KEY
# - DJANGO_ALLOWED_HOSTS
# - CELERY_BROKER_URL
# - POSTGRES_* (database ayarları)
```
### Başlatma
```bash
# Tüm servisleri başlat (Django + Celery + Nginx)
docker-compose -f docker-compose.prod.yml up -d
# Build ile başlat
docker-compose -f docker-compose.prod.yml up --build -d
# Logları kontrol et
docker-compose -f docker-compose.prod.yml logs -f
# Celery logları
docker-compose -f docker-compose.prod.yml logs -f celery-atahan
```
### Servis Durumları
```bash
# Container durumlarını kontrol et
docker-compose -f docker-compose.prod.yml ps
# Kaynak kullanımı
docker stats
```
### Durdurma
```bash
# Servisleri durdur
docker-compose -f docker-compose.prod.yml down
# Yeniden başlat
docker-compose -f docker-compose.prod.yml restart
# Sadece Celery'yi yeniden başlat
docker-compose -f docker-compose.prod.yml restart celery-atahan
```
## 🔧 Yararlı Komutlar
### Migrations
```bash
# Development
docker-compose exec web python manage.py makemigrations
docker-compose exec web python manage.py migrate
# Production
docker-compose -f docker-compose.prod.yml exec web-atahan python manage.py migrate
```
### Django Shell
```bash
# Development
docker-compose exec web python manage.py shell
# Production
docker-compose -f docker-compose.prod.yml exec web-atahan python manage.py shell
```
### Celery Container'a Bağlanma
```bash
# Development
docker-compose exec celery bash
# Production
docker-compose -f docker-compose.prod.yml exec celery-atahan bash
```
### Task Sonuçlarını Görme
```bash
# Shell aç
docker-compose exec web python manage.py shell
# Shell içinde:
from django_celery_results.models import TaskResult
TaskResult.objects.all()
TaskResult.objects.filter(status='SUCCESS')
TaskResult.objects.order_by('-date_done')[:10]
```
## 📊 İzleme
### Container Logları
```bash
# Tüm servisler (Development)
docker-compose logs -f
# Tüm servisler (Production)
docker-compose -f docker-compose.prod.yml logs -f
# Son 100 satır
docker-compose logs --tail=100 celery
# Belirli zaman aralığı
docker-compose logs --since 2026-01-15T10:00:00 celery
```
### Celery İstatistikleri
```bash
# Aktif task'lar
docker-compose exec celery celery -A core inspect active
# Worker istatistikleri
docker-compose exec celery celery -A core inspect stats
# Kayıtlı task'lar
docker-compose exec celery celery -A core inspect registered
```
### Django Admin
1. **Admin panele gir:**
- Development: http://localhost:8000/admin/
- Production: http://your-domain.com/admin/
2. **Task sonuçlarını gör:**
- Django Celery Results → Task results
3. **Periyodik task'ları yönet:**
- Django Celery Beat → Periodic tasks
## 🐛 Sorun Giderme
### Celery Çalışmıyor
```bash
# 1. Logları kontrol et
docker-compose logs celery
# 2. Container durumunu kontrol et
docker-compose ps
# 3. Container'ı yeniden başlat
docker-compose restart celery
# 4. Rebuild et
docker-compose up --build celery
```
### Email Gönderilmiyor
```bash
# 1. Celery loglarını kontrol et
docker-compose logs -f celery
# 2. MailPit çalışıyor mu? (Development)
open http://localhost:8025
# 3. Email ayarlarını kontrol et
docker-compose exec web python manage.py shell
>>> from django.conf import settings
>>> print(settings.EMAIL_BACKEND)
>>> print(settings.EMAIL_HOST)
```
### Redis Bağlantı Hatası
```bash
# Environment variable'ları kontrol et
docker-compose exec celery env | grep CELERY
# Redis bağlantısını test et
docker-compose exec celery python -c "
from django.conf import settings
print(settings.CELERY_BROKER_URL)
"
```
## 📚 Servis Portları
### Development
- **Django**: http://localhost:8000
- **MailPit UI**: http://localhost:8025
- **MailPit SMTP**: localhost:1025
### Production
- **Nginx**: http://localhost:8077
- **Django (Direct)**: http://localhost:8800
## 🔐 Environment Variables
### Development (.env veya docker-compose.yml)
```bash
DEBUG=1
CELERY_BROKER_URL=redis://default:password@host:6379/5
CELERY_RESULT_BACKEND=django-db
POSTGRES_DB=server_dj
POSTGRES_USER=server_dj
POSTGRES_PASSWORD=1234
POSTGRES_HOST=10.80.80.50
POSTGRES_PORT=5432
```
### Production (.env)
```bash
DEBUG=0
SECRET_KEY=your-secret-key-here
DJANGO_ALLOWED_HOSTS=yourdomain.com
CELERY_BROKER_URL=redis://default:password@host:6379/5
CELERY_RESULT_BACKEND=django-db
POSTGRES_DB=your-db
POSTGRES_USER=your-user
POSTGRES_PASSWORD=your-password
POSTGRES_HOST=your-host
POSTGRES_PORT=5432
```
## 📖 Detaylı Dökümanlar
- **DOCKER_CELERY.md** - Celery kullanımı hakkında detaylı bilgi
- **DOCKER_README.md** - Docker genel kullanımı
- **CONTACT_EMAIL_SETUP.md** - Contact email kurulumu
## 🎯 Önemli Notlar
1.**Celery worker ve beat** aynı container'da çalışır
2.**Redis** harici olarak çalışmalı (CELERY_BROKER_URL)
3.**PostgreSQL** harici olarak çalışmalı
4.**MailPit** development için email testleri sağlar
5. ✅ Production'da gerçek SMTP kullanın
## 🚨 İlk Başlatmada Yapılacaklar
```bash
# 1. Servisleri başlat
docker-compose up -d
# 2. Migration'ları uygula
docker-compose exec web python manage.py migrate
# 3. Superuser oluştur
docker-compose exec web python manage.py createsuperuser
# 4. Static dosyaları topla (Production)
docker-compose exec web python manage.py collectstatic --noinput
# 5. Test et
curl -X POST http://localhost:8000/api/v1/contact/create/ \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com","subject":"Test","message":"Test"}'
# 6. Logları kontrol et
docker-compose logs -f celery
```
---
**Yardım**: Sorun yaşarsanız `DOCKER_CELERY.md` dosyasına bakın veya logları kontrol edin.

View File

@@ -0,0 +1,269 @@
# 🎉 Docker Compose Production Test Raporu
**Test Tarihi:** 15 Ocak 2026, 17:12
**Ortam:** Production (docker-compose.prod.yml)
## ✅ Sorun ve Çözüm
### 🐛 Tespit Edilen Sorun
```
AttributeError: 'zoneinfo.ZoneInfo' object has no attribute 'localize'
```
**Sebep:**
- Django Celery Beat'in Python 3.14'teki yeni `zoneinfo` modülü ile uyumsuzluğu
- `pytz` yerine `zoneinfo` kullanımındaki API değişikliği
### ✅ Uygulanan Çözüm
1. **Beat Scheduler Kaldırıldı**
- Worker ve Beat ayrıldı
- Worker: `celery -A core worker --loglevel=info`
- Beat: Opsiyonel (ayrı dosya: `docker-compose.celery-beat.yml`)
2. **Timezone Ayarları Güncellendi**
```python
CELERY_TIMEZONE = 'Europe/Istanbul'
CELERY_ENABLE_UTC = True
```
3. **Docker Compose Güncellemeleri**
- `docker-compose.yml` - Development (beat yok)
- `docker-compose.prod.yml` - Production (beat yok)
- `docker-compose.celery-beat.yml` - Beat için opsiyonel
## 📊 Production Test Sonuçları
### Container Durumları
| Container | Status | Ports | CPU/Memory |
|-----------|--------|-------|------------|
| django_web_prod_atahan | ✅ Running | 0.0.0.0:8800->8000 | Normal |
| django_celery_prod_atahan | ✅ Running | - | Normal |
| django_nginx_atahan | ✅ Running | 0.0.0.0:8077->80 | Normal |
### Servis Testleri
#### 1. Django Web (Gunicorn)
```
✅ Gunicorn: 3 workers aktif
✅ Port: 8800 (direct), 8077 (nginx)
✅ Database: Bağlı
✅ Migrations: Uygulandı
✅ Static files: Toplanan (365 dosya)
```
**Log:**
```
[2026-01-15 14:11:24 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-01-15 14:11:24 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000
[2026-01-15 14:11:24 +0000] [1] [INFO] Using worker: sync
[2026-01-15 14:11:24 +0000] [10] [INFO] Booting worker with pid: 10
[2026-01-15 14:11:24 +0000] [11] [INFO] Booting worker with pid: 11
[2026-01-15 14:11:24 +0000] [12] [INFO] Booting worker with pid: 12
```
#### 2. Celery Worker
```
✅ Worker: Çalışıyor
✅ Concurrency: 8 workers (prefork)
✅ Redis: Bağlı (212.64.215.243:6379/5)
✅ Tasks: Yüklendi
✅ Beat: YOK (ayrıldı)
```
**Yüklü Task'lar:**
- ✅ `contact.tasks.send_contact_email`
- ✅ `core.celery.debug_task`
- ✅ `imagekit.cachefiles.backends._generate_file`
**Log:**
```
[2026-01-15 17:11:30,001: INFO/MainProcess] Connected to redis://default:**@212.64.215.243:6379/5
[2026-01-15 17:11:31,352: INFO/MainProcess] celery@56445c300966 ready.
```
#### 3. Nginx Reverse Proxy
```
✅ Status: Running
✅ Port: 8077
✅ Upstream: django_web_prod_atahan:8000
✅ Configuration: Valid
```
### API Test Sonuçları
#### Test 1: Direct Gunicorn (Port 8800)
```bash
POST http://localhost:8800/api/v1/contact/create/
{
"name": "Production Test",
"email": "prod@test.com",
"subject": "Production Docker Test",
"message": "Bu production ortamından gönderilen bir test mesajıdır."
}
```
**Response:** ✅ 201 Created
```json
{
"id": 10,
"name": "Production Test",
"email": "prod@test.com",
"subject": "Production Docker Test",
"message": "Bu production ortamından gönderilen bir test mesajıdır.",
"created_at": "2026-01-15T17:12:24.550815+03:00"
}
```
#### Test 2: Celery Task Execution
```
✅ Task Received: contact.tasks.send_contact_email
✅ Task ID: 482451a6-d2b1-4609-ad02-b97b155c4c75
✅ Status: SUCCESS (0.066s)
✅ Worker: ForkPoolWorker-7
```
**Celery Log:**
```
[2026-01-15 17:12:24,519: INFO/MainProcess] Task contact.tasks.send_contact_email[...] received
[2026-01-15 17:12:24,587: INFO/ForkPoolWorker-7] Task contact.tasks.send_contact_email[...] succeeded in 0.065s
```
**Not:** Email SMTP hatası (beklenen - MailPit bağlı değil):
```
'Email gönderilemedi: [Errno 111] Connection refused'
```
## 📈 Performans Metrikleri
| Metrik | Değer | Durum |
|--------|-------|-------|
| Container Start Time | ~10s | ✅ İyi |
| API Response Time | <1s | ✅ Mükemmel |
| Celery Task Exec | 0.065s | ✅ Mükemmel |
| Gunicorn Workers | 3 | ✅ Optimal |
| Celery Concurrency | 8 | ✅ Optimal |
## 🔧 Yapılandırma Değişiklikleri
### docker-compose.yml (Development)
```yaml
celery:
command: celery -A core worker --loglevel=info
# --beat kaldırıldı
```
### docker-compose.prod.yml (Production)
```yaml
celery-atahan:
command: celery -A core worker --loglevel=info
# --beat --scheduler django kaldırıldı
```
### core/settings.py
```python
CELERY_TIMEZONE = 'Europe/Istanbul' # UTC yerine
CELERY_ENABLE_UTC = True # Yeni eklendi
```
### Yeni Dosya: docker-compose.celery-beat.yml
```yaml
# Periyodik task'lar için ayrı beat container
# Kullanım: docker-compose -f docker-compose.yml -f docker-compose.celery-beat.yml up -d
```
## 🎯 Sonuç
### ✅ Çalışan Özellikler
1. ✅ Production web server (Gunicorn)
2. ✅ Nginx reverse proxy
3. ✅ Celery worker (8 concurrent)
4. ✅ Asenkron task execution
5. ✅ Contact API endpoint
6. ✅ Database connection
7. ✅ Redis broker connection
8. ✅ Static file serving
9. ✅ Auto-restart (restart: unless-stopped)
10. ✅ Multi-container orchestration
### ⚠️ Notlar
1. **Email Gönderimi:** MailPit production'da yok (SMTP yapılandırması gerekli)
2. **Beat Scheduler:** Şu an devre dışı (ihtiyaç olursa ayrı container'da çalıştırılabilir)
3. **Timezone Uyarısı:** Çözüldü
### 📝 Production'a Almak İçin
1. **.env Dosyası:**
```bash
cp .env.example .env
nano .env
# Gereken değerler:
DEBUG=0
SECRET_KEY=<your-secret-key>
DJANGO_ALLOWED_HOSTS=yourdomain.com
CELERY_BROKER_URL=redis://...
```
2. **Email SMTP Ayarları:**
```python
# settings.py (production)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'app-password'
```
3. **Başlatma:**
```bash
docker-compose -f docker-compose.prod.yml up -d
docker-compose -f docker-compose.prod.yml logs -f
```
## 🎊 Final Durum
**✅ PRODUCTION DOCKER COMPOSE HAZIR!**
| Component | Status | Notes |
|-----------|--------|-------|
| Development | ✅ Test Edildi | docker-compose.yml |
| Production | ✅ Test Edildi | docker-compose.prod.yml |
| Celery Worker | ✅ Çalışıyor | Beat ayrıldı |
| Nginx | ✅ Çalışıyor | Reverse proxy aktif |
| API | ✅ Çalışıyor | Contact endpoint |
| Tasks | ✅ Çalışıyor | Email task execution |
| Beat Scheduler | ⏸️ Opsiyonel | Ayrı dosyada |
---
**Sorun Çözüldü:** Beat scheduler hatası düzeltildi
**Test Durumu:** BAŞARILI ✅
**Production Ready:** EVET ✅
## 🚀 Hızlı Komutlar
```bash
# Production başlat
docker-compose -f docker-compose.prod.yml up -d
# Logları izle
docker-compose -f docker-compose.prod.yml logs -f celery-atahan
# Container durumları
docker ps --filter "name=atahan"
# Test
curl -X POST http://localhost:8800/api/v1/contact/create/ \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com","subject":"Test","message":"Test"}'
# Durdur
docker-compose -f docker-compose.prod.yml down
```
🎉 **BAŞARIYLA TAMAMLANDI!**

227
DOCKER_README.md Normal file
View File

@@ -0,0 +1,227 @@
# Django Projesi - Docker Kurulum Rehberi
Bu Django projesi Python 3.14.2 ile dockerize edilmiştir.
## 📋 Gereksinimler
- Docker
- Docker Compose
## 🚀 Hızlı Başlangıç
### Geliştirme Ortamı (Development)
1. **Projeyi klonlayın ve dizine girin:**
```bash
cd /path/to/project
```
2. **Docker container'ları başlatın:**
```bash
docker-compose up --build
```
3. **Tarayıcınızda açın:**
```
http://localhost:8000
```
### Production Ortamı
1. **Environment dosyasını oluşturun:**
```bash
cp .env.example .env
# .env dosyasını düzenleyin ve gerçek değerleri girin
```
2. **Production container'ları başlatın:**
```bash
docker-compose -f docker-compose.prod.yml up --build -d
```
3. **Nginx üzerinden erişin:**
```
http://localhost
```
## 🛠️ Yararlı Komutlar
### Container'ları Başlatma
```bash
# Geliştirme
docker-compose up
# Production
docker-compose -f docker-compose.prod.yml up -d
# Rebuild ile başlatma
docker-compose up --build
```
### Container'ları Durdurma
```bash
docker-compose down
# Volume'leri de silmek için
docker-compose down -v
```
### Django Komutları Çalıştırma
```bash
# Migration oluşturma
docker-compose exec web python manage.py makemigrations
# Migration uygulama
docker-compose exec web python manage.py migrate
# Superuser oluşturma
docker-compose exec web python manage.py createsuperuser
# Shell açma
docker-compose exec web python manage.py shell
# Static dosyaları toplama
docker-compose exec web python manage.py collectstatic
```
### Celery Komutları
```bash
# Celery worker loglarını görüntüleme
docker-compose logs -f celery
# Production ortamında
docker-compose -f docker-compose.prod.yml logs -f celery-atahan
# Celery worker'ı yeniden başlatma
docker-compose restart celery
# Celery container'a bağlanma
docker-compose exec celery bash
# Celery task durumlarını kontrol etme (Django shell içinde)
docker-compose exec web python manage.py shell
# >>> from django_celery_results.models import TaskResult
# >>> TaskResult.objects.all()
```
### Logları Görüntüleme
```bash
# Tüm servislerin logları
docker-compose logs -f
# Sadece web servisinin logları
docker-compose logs -f web
# Sadece database logları
docker-compose logs -f db
```
### Container'a Bağlanma
```bash
# Web container'a bash ile bağlan
docker-compose exec web bash
# Database container'a bağlan
docker-compose exec db psql -U server_dj -d server_dj
```
## 📁 Proje Yapısı
```
.
├── Dockerfile # Ana Docker image tanımı
├── docker-compose.yml # Geliştirme ortamı yapılandırması
├── docker-compose.prod.yml # Production ortamı yapılandırması
├── entrypoint.sh # Container başlatma scripti
├── nginx.conf # Nginx yapılandırması (production)
├── .dockerignore # Docker'a dahil edilmeyecek dosyalar
├── .env.example # Environment değişkenleri şablonu
└── requirements.txt # Python bağımlılıkları
```
## 🔧 Konfigürasyon
### Environment Değişkenleri
`.env` dosyasında aşağıdaki değişkenleri ayarlayabilirsiniz:
- `DEBUG`: Debug modu (0 veya 1)
- `SECRET_KEY`: Django secret key
- `DJANGO_ALLOWED_HOSTS`: İzin verilen host'lar
- `POSTGRES_DB`: PostgreSQL veritabanı adı
- `POSTGRES_USER`: PostgreSQL kullanıcı adı
- `POSTGRES_PASSWORD`: PostgreSQL şifresi
### Veritabanı
Proje hem SQLite hem de PostgreSQL destekler:
- **Development**: SQLite (varsayılan)
- **Production**: PostgreSQL (docker-compose ile)
### Static ve Media Dosyaları
- Static dosyalar: `/app/staticfiles`
- Media dosyaları: `/app/media`
- Her ikisi de Docker volume'lerinde saklanır
## 🔐 Güvenlik
Production ortamında:
1. `.env` dosyasındaki tüm varsayılan şifreleri değiştirin
2. `SECRET_KEY` için güçlü bir değer kullanın
3. `DEBUG=0` olarak ayarlayın
4. `ALLOWED_HOSTS` değerini doğru domain ile güncelleyin
5. SSL sertifikası ekleyin (nginx yapılandırmasına)
## 📊 Veritabanı Yedekleme
### PostgreSQL Backup
```bash
# Backup alma
docker-compose exec db pg_dump -U server_dj server_dj > backup.sql
# Backup geri yükleme
docker-compose exec -T db psql -U server_dj server_dj < backup.sql
```
## 🐛 Sorun Giderme
### Port zaten kullanımda
```bash
# Port 8000'i kullanan process'i bul
lsof -i :8000
# Veya farklı port kullan
# docker-compose.yml'de ports kısmını değiştirin
```
### Container başlamıyor
```bash
# Logları kontrol et
docker-compose logs web
# Container'ları temizle ve yeniden başlat
docker-compose down -v
docker-compose up --build
```
### Static dosyalar yüklenmiyor
```bash
# Static dosyaları yeniden topla
docker-compose exec web python manage.py collectstatic --noinput --clear
```
## 📝 Notlar
- İlk çalıştırmada `entrypoint.sh` otomatik olarak:
- Database migration'larını uygular
- Admin kullanıcısı oluşturur (admin/admin)
- Static dosyaları toplar
- Development ortamında kod değişiklikleri otomatik olarak yansır (volume mount sayesinde)
## 📞 Destek
Herhangi bir sorun için issue açabilirsiniz.

183
DOCKER_TEST_REPORT.md Normal file
View File

@@ -0,0 +1,183 @@
# 🎉 Docker Compose Test Raporu
**Test Tarihi:** 15 Ocak 2026
**Test Ortamı:** Development (docker-compose.yml)
## ✅ Test Sonuçları
### 1. Container Durumu
```
✅ django_web - Running (Up 2 minutes)
✅ django_celery_worker - Running (Up 2 minutes)
```
### 2. Django Web Servisi
- ✅ Port: http://localhost:8000
- ✅ Database migrations: Başarılı
- ✅ Static files: Toplanan (365 dosya)
- ✅ Superuser: Mevcut
- ✅ Development server: Çalışıyor
**Log Özeti:**
```
Django version 6.0, using settings 'core.settings'
Starting development server at http://0.0.0.0:8000/
System check identified no issues (0 silenced).
```
### 3. Celery Worker + Beat
- ✅ Worker: Çalışıyor
- ✅ Beat Scheduler: Çalışıyor
- ✅ Redis Connection: Başarılı
- ✅ Concurrency: 8 workers
- ✅ Task Discovery: Başarılı
**Yüklenen Task'lar:**
```
✅ contact.tasks.send_contact_email
✅ core.celery.debug_task
✅ imagekit.cachefiles.backends._generate_file
```
**Celery Bilgileri:**
```
- App: core
- Transport: redis://default:**@212.64.215.243:6379/5
- Concurrency: 8 (prefork)
- Queue: celery
```
### 4. Contact API Test
**Test Request:**
```bash
POST http://localhost:8000/api/v1/contact/create/
Content-Type: application/json
{
"name": "Test Kullanıcı Docker",
"email": "test@docker.com",
"subject": "Docker Test",
"message": "Bu Docker Compose üzerinden gönderilen bir test mesajıdır."
}
```
**Response:** ✅ 201 Created
```json
{
"id": 8,
"name": "Test Kullanıcı Docker",
"email": "test@docker.com",
"subject": "Docker Test",
"message": "Bu Docker Compose üzerinden gönderilen bir test mesajıdır.",
"created_at": "2026-01-15T17:03:02.934766+03:00"
}
```
### 5. Celery Task Execution
**Task Result:**
```
Total tasks: 1
Status: SUCCESS
Date Done: 2026-01-15 14:03:03.121846+00:00
```
**Email task başarıyla çalıştı!**
## 📊 Performans
- **Container Start Time:** ~5 saniye
- **API Response Time:** < 1 saniye
- **Celery Task Execution:** < 1 saniye
- **Total Memory:** Optimized
## 🔍 Tespit Edilen Uyarılar
### 1. Celery Beat Scheduler Uyarısı
```
AttributeError: 'zoneinfo.ZoneInfo' object has no attribute 'localize'
```
**Açıklama:**
- Django Celery Beat ile Python 3.14'teki zoneinfo uyumsuzluğu
- Sadece `celery.backend_cleanup` task'ı için
- **Etki:** Düşük - Worker çalışıyor, custom task'lar çalışıyor
- **Çözüm:** İleriki versiyonlarda düzeltilecek veya timezone ayarları güncellenecek
### 2. Django Celery Beat Migration Uyarısı
```
Your models in app(s): 'django_celery_beat' have changes that are not yet reflected in a migration
```
**Çözüm:**
```bash
docker exec django_web python manage.py makemigrations django_celery_beat
docker exec django_web python manage.py migrate
```
## 🎯 Test Sonucu: BAŞARILI ✅
### Çalışan Özellikler:
1. ✅ Docker Compose multi-container setup
2. ✅ Django web application
3. ✅ Celery worker + beat
4. ✅ Contact API endpoint
5. ✅ Asenkron email gönderimi
6. ✅ Redis broker bağlantısı
7. ✅ PostgreSQL database bağlantısı
8. ✅ Task result tracking
9. ✅ Static file serving
10. ✅ Hot reload (development)
## 🚀 Kullanım
### Başlatma
```bash
docker-compose up -d
```
### Log İzleme
```bash
# Tüm loglar
docker-compose logs -f
# Sadece Celery
docker-compose logs -f celery
# Sadece Web
docker-compose logs -f web
```
### Durdurma
```bash
docker-compose down
```
### Test
```bash
curl -X POST http://localhost:8000/api/v1/contact/create/ \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com","subject":"Test","message":"Test message"}'
```
## 📝 Notlar
1. **Development Ortamı:** Bu test development ortamında yapıldı
2. **Production:** Production testleri için `docker-compose.prod.yml` kullanılmalı
3. **Email:** MailPit çalışmıyor olabilir, production'da gerçek SMTP kullanın
4. **Redis:** External Redis kullanılıyor (212.64.215.243:6379)
5. **PostgreSQL:** External PostgreSQL kullanılıyor (10.80.80.50:5432)
## 🎊 Sonuç
Docker Compose yapılandırması başarıyla test edildi. Celery worker ve beat scheduler çalışıyor, contact form email gönderimi aktif.
**Status:** ✅ PRODUCTION READY (Uyarılar düzeltildikten sonra)
---
**Test Eden:** Docker Compose Test Suite
**Tarih:** 2026-01-15 17:03
**Ortam:** macOS + Docker Desktop

View File

@@ -0,0 +1,135 @@
# Django Projesi - Docker ile Mevcut PostgreSQL Kullanımı
**Projeniz başarıyla dockerize edildi ve çalışıyor!**
Bu yapılandırma, mevcut PostgreSQL sunucunuzu (10.80.80.50:5432) kullanarak Django projenizi Docker'da çalıştırır.
## 🎉 Test Edildi ve Çalışıyor
Server `http://localhost:8000` adresinde çalışıyor!
**Oluşturulan Admin Kullanıcısı:**
- Email: `admin@example.com`
- Şifre: `admin`
## ✅ Yapılan Değişiklikler
1. **PostgreSQL Container'ı kaldırıldı** - Mevcut sunucunuz kullanılacak
2. **[settings.py](core/settings.py)** - Environment değişkenleri ile PostgreSQL yapılandırması
3. **[docker-compose.yml](docker-compose.yml)** - Sadece web servisi (mevcut PostgreSQL'e bağlanır)
4. **[docker-compose.prod.yml](docker-compose.prod.yml)** - Production yapılandırması güncellendi
5. **[entrypoint.sh](entrypoint.sh)** - PostgreSQL bekleme kodu kaldırıldı
## 🚀 Kullanım
### Geliştirme Ortamı
```bash
# Docker container'ı başlat
docker-compose up --build
# Tarayıcıda aç
# http://localhost:8000
```
Container otomatik olarak 10.80.80.50:5432 adresindeki PostgreSQL sunucunuza bağlanacak.
### Production Ortamı
```bash
# .env dosyasını oluştur
cp .env.example .env
# .env dosyasını düzenle (gerekirse PostgreSQL bilgilerini güncelle)
# Production container'ları başlat
docker-compose -f docker-compose.prod.yml up -d
```
## 🔧 PostgreSQL Bağlantı Ayarları
Docker container'ınız şu ayarlarla PostgreSQL'e bağlanır:
```
Host: 10.80.80.50
Port: 5432
Database: server_dj
User: server_dj
Password: 1234
```
Bu ayarları değiştirmek için:
**Geliştirme:** [docker-compose.yml](docker-compose.yml) içindeki environment değişkenlerini düzenleyin
**Production:** `.env` dosyasını düzenleyin
### SQLite Kullanmak İsterseniz
```bash
# docker-compose.yml içinde USE_POSTGRES değişkenini değiştirin:
- USE_POSTGRES=False
```
## 📋 Yararlı Komutlar
```bash
# Migration uygula
docker-compose exec web python manage.py migrate
# Superuser oluştur
docker-compose exec web python manage.py createsuperuser
# Shell aç
docker-compose exec web python manage.py shell
# Logları görüntüle
docker-compose logs -f web
# Container'ı durdur
docker-compose down
```
## 🔍 Sorun Giderme
### PostgreSQL'e bağlanamıyorum
1. PostgreSQL sunucusunun çalıştığından emin olun
2. Docker container'ından 10.80.80.50:5432 adresine erişilebildiğini kontrol edin:
```bash
docker-compose exec web bash
apt-get update && apt-get install -y postgresql-client
psql -h 10.80.80.50 -U server_dj -d server_dj
```
### Mac'te Docker Network Sorunu
Mac'te Docker Desktop kullanıyorsanız ve localhost PostgreSQL'e bağlanamıyorsanız:
[docker-compose.yml](docker-compose.yml) içinde:
```yaml
environment:
- POSTGRES_HOST=host.docker.internal # 10.80.80.50 yerine
```
## 📁 Dosya Yapısı
```
.
├── Dockerfile # Django container image
├── docker-compose.yml # Geliştirme (mevcut PostgreSQL kullanır)
├── docker-compose.prod.yml # Production (mevcut PostgreSQL kullanır)
├── entrypoint.sh # Container başlatma scripti
├── nginx.conf # Nginx config (production)
├── .env.example # Environment değişkenleri
└── core/
└── settings.py # PostgreSQL ayarları (env değişkenlerinden)
```
## Notlar
- Container içinden `10.80.80.50` adresine erişmek için ağ yapılandırmanızın buna izin vermesi gerekir
- Production ortamında `.env` dosyasındaki şifreleri mutlaka değiştirin
- İlk çalıştırmada migrations otomatik uygulanır
- Static dosyalar otomatik toplanır

42
Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# Python 3.14.2 base image kullan
FROM python:3.14.2-slim
# Çalışma ortamı değişkenlerini ayarla
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Çalışma dizinini oluştur
WORKDIR /app
# Sistem bağımlılıklarını yükle (PostgreSQL ve diğer gerekli paketler için)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
postgresql-client \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Python bağımlılıklarını kopyala ve yükle
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Proje dosyalarını kopyala
COPY . .
# Static dosyaları topla
RUN python manage.py collectstatic --noinput --clear || true
# Media ve staticfiles dizinlerini oluştur
RUN mkdir -p /app/media /app/staticfiles
# Port 8000'i aç
EXPOSE 8000
# Entrypoint scriptini çalıştırılabilir yap
RUN chmod +x /app/entrypoint.sh || true
RUN chmod +x /app/entrypoint-celery.sh || true
# Entrypoint ve varsayılan komut
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

475
FRONTEND_INTEGRATION.md Normal file
View File

@@ -0,0 +1,475 @@
# Frontend Integration Guide (Nuxt.js / Next.js)
## 🎯 Architecture
```
Frontend (Nuxt/Next.js) Backend (Django)
Port: 3000 Port: 8000
├── Pages/Routes ├── API Endpoints
├── UI/UX ├── Authentication
├── API Calls ├── Database
└── Token Storage └── Business Logic
```
---
## 📧 Email Links Flow
### How It Works:
1. **User registers** → Backend sends email
2. **Email contains** → Frontend URL (http://localhost:3000/activate/...)
3. **User clicks link** → Opens Frontend page
4. **Frontend JavaScript** → Calls Backend API
5. **Backend** → Activates account, returns response
6. **Frontend** → Shows success message
### Email Link Format:
```
Activation: http://localhost:3000/activate/{uid}/{token}/
Password Reset: http://localhost:3000/password-reset/{uid}/{token}/
```
---
## 🚀 Nuxt.js Implementation
### 1. Environment Variables (`.env`)
```bash
# Nuxt.js .env
NUXT_PUBLIC_API_BASE=http://localhost:8000/api/v1
```
### 2. Nuxt Config (`nuxt.config.ts`)
```typescript
export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8000/api/v1'
}
},
// CORS configuration for development
nitro: {
devProxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
})
```
### 3. API Composable (`composables/useApi.ts`)
```typescript
export const useApi = () => {
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
return {
apiBase,
async fetch(endpoint: string, options: any = {}) {
return await $fetch(`${apiBase}${endpoint}`, options)
}
}
}
```
### 4. Auth Composable (`composables/useAuth.ts`)
```typescript
export const useAuth = () => {
const { apiBase } = useApi()
const router = useRouter()
// Register
const register = async (userData: {
email: string
password: string
re_password: string
first_name: string
last_name: string
}) => {
return await $fetch(`${apiBase}/auth/users/`, {
method: 'POST',
body: userData
})
}
// Activate Account
const activate = async (uid: string, token: string) => {
return await $fetch(`${apiBase}/auth/users/activation/`, {
method: 'POST',
body: { uid, token }
})
}
// Login
const login = async (email: string, password: string) => {
const data = await $fetch(`${apiBase}/auth/jwt/create/`, {
method: 'POST',
body: { email, password }
})
// Save tokens
localStorage.setItem('access_token', data.access)
localStorage.setItem('refresh_token', data.refresh)
return data
}
// Social Login
const socialLogin = async (provider: string, accessToken: string) => {
const data = await $fetch(`${apiBase}/auth/social/${provider}/`, {
method: 'POST',
body: { access_token: accessToken }
})
// Save JWT tokens
localStorage.setItem('access_token', data.access)
localStorage.setItem('refresh_token', data.refresh)
return data
}
// Get Current User
const getUser = async () => {
const token = localStorage.getItem('access_token')
if (!token) return null
return await $fetch(`${apiBase}/auth/users/me/`, {
headers: {
Authorization: `Bearer ${token}`
}
})
}
// Logout
const logout = () => {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
router.push('/login')
}
return {
register,
activate,
login,
socialLogin,
getUser,
logout
}
}
```
### 5. Activation Page (`pages/activate/[uid]/[token].vue`)
```vue
<template>
<div class="activation-page">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<h1>Activating Your Account...</h1>
<p>Please wait while we activate your account.</p>
</div>
<div v-else-if="success" class="success">
<div class="icon"></div>
<h1>Account Activated!</h1>
<p>Your account has been successfully activated.</p>
<NuxtLink to="/login" class="btn">Go to Login</NuxtLink>
</div>
<div v-else class="error">
<div class="icon"></div>
<h1>Activation Failed</h1>
<p>{{ error }}</p>
<NuxtLink to="/login" class="btn">Back to Login</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { activate } = useAuth()
const loading = ref(true)
const success = ref(false)
const error = ref('')
onMounted(async () => {
const uid = route.params.uid as string
const token = route.params.token as string
try {
await activate(uid, token)
success.value = true
} catch (e: any) {
error.value = e.data?.detail || e.data?.token?.[0] || 'Activation failed'
} finally {
loading.value = false
}
})
</script>
<style scoped>
.activation-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
margin: 0 auto 2rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
```
### 6. Register Page (`pages/register.vue`)
```vue
<template>
<div class="register-page">
<div class="card">
<h1>Create Account</h1>
<form @submit.prevent="handleRegister">
<input v-model="form.email" type="email" placeholder="Email" required />
<input v-model="form.first_name" placeholder="First Name" required />
<input v-model="form.last_name" placeholder="Last Name" required />
<input v-model="form.password" type="password" placeholder="Password" required />
<input v-model="form.re_password" type="password" placeholder="Confirm Password" required />
<button type="submit">Register</button>
</form>
<div v-if="registered" class="success">
Registration successful! Please check your email to activate your account.
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const { register } = useAuth()
const form = ref({
email: '',
password: '',
re_password: '',
first_name: '',
last_name: ''
})
const registered = ref(false)
const error = ref('')
const handleRegister = async () => {
try {
await register(form.value)
registered.value = true
} catch (e: any) {
error.value = Object.values(e.data).flat().join(', ')
}
}
</script>
```
### 7. Login Page (`pages/login.vue`)
```vue
<template>
<div class="login-page">
<div class="card">
<h1>Login</h1>
<form @submit.prevent="handleLogin">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
<div class="divider">OR</div>
<button @click="loginWithGoogle" class="btn-google">
Continue with Google
</button>
<div v-if="error" class="error">{{ error }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const { login, socialLogin } = useAuth()
const router = useRouter()
const email = ref('')
const password = ref('')
const error = ref('')
const handleLogin = async () => {
try {
await login(email.value, password.value)
router.push('/dashboard')
} catch (e: any) {
error.value = e.data?.detail || 'Login failed'
}
}
const loginWithGoogle = async () => {
// Implement Google OAuth (use @nuxtjs/google-oauth2 or similar)
const googleToken = await getGoogleAccessToken()
try {
await socialLogin('google-oauth2', googleToken)
router.push('/dashboard')
} catch (e: any) {
error.value = e.data?.error || 'Social login failed'
}
}
</script>
```
---
## 🔐 Protected Pages (Middleware)
### Auth Middleware (`middleware/auth.ts`)
```typescript
export default defineNuxtRouteMiddleware((to, from) => {
const token = process.client ? localStorage.getItem('access_token') : null
if (!token) {
return navigateTo('/login')
}
})
```
### Dashboard Page (`pages/dashboard.vue`)
```vue
<template>
<div class="dashboard">
<h1>Welcome, {{ user?.first_name }}!</h1>
<p>Email: {{ user?.email }}</p>
<button @click="logout">Logout</button>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
const { getUser, logout } = useAuth()
const user = ref(null)
onMounted(async () => {
user.value = await getUser()
})
</script>
```
---
## 🌐 Next.js Implementation
Very similar to Nuxt.js, just adjust the syntax:
```typescript
// app/activate/[uid]/[token]/page.tsx
'use client'
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
export default function ActivatePage() {
const params = useParams()
const [loading, setLoading] = useState(true)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const activate = async () => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE}/auth/users/activation/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
uid: params.uid,
token: params.token
})
}
)
if (response.ok) {
setSuccess(true)
} else {
const data = await response.json()
setError(data.detail || 'Activation failed')
}
} catch (e) {
setError('Network error')
} finally {
setLoading(false)
}
}
activate()
}, [params])
if (loading) return <div>Activating...</div>
if (success) return <div> Account Activated!</div>
return <div> {error}</div>
}
```
---
## 📝 Summary
### Email Links:
- Activation: `http://localhost:3000/activate/{uid}/{token}/`
- Password Reset: `http://localhost:3000/password-reset/{uid}/{token}/`
### API Endpoints (Backend):
- Register: `POST http://localhost:8000/api/v1/auth/users/`
- Activate: `POST http://localhost:8000/api/v1/auth/users/activation/`
- Login: `POST http://localhost:8000/api/v1/auth/jwt/create/`
- Social Login: `POST http://localhost:8000/api/v1/auth/social/{provider}/`
- Current User: `GET http://localhost:8000/api/v1/auth/users/me/`
### Production URLs:
- Frontend: `https://yourdomain.com`
- Backend: `https://api.yourdomain.com`
Update `DOMAIN` in Django settings for production!
---
**Happy Coding! 🚀**

183
QUICK_START.md Normal file
View File

@@ -0,0 +1,183 @@
# 🚀 Quick Start Guide
Django REST API Authentication sistemi başarıyla kuruldu! İşte hızlı başlangıç rehberi:
## ✅ Kurulum Tamamlandı
Sistem şu anda çalışır durumda:
- ✅ Custom User Model (email-based)
- ✅ JWT Authentication
- ✅ Email Activation
- ✅ Social Login (Google, GitHub, Facebook)
- ✅ Password Reset
- ✅ Admin Panel
## 🎯 Hemen Test Et
### 1. Server Çalıştır
```bash
cd /home/beyhan/Python/server
source .venv/bin/activate
python manage.py runserver
```
### 2. Admin Panel'e Giriş Yap
```
URL: http://localhost:8000/admin/
Email: admin@example.com
Password: admin123
```
### 3. API Test Et
**Register (Kayıt):**
```bash
curl -X POST http://localhost:8000/api/v1/auth/users/ \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "TestP@ss123",
"re_password": "TestP@ss123",
"first_name": "Test",
"last_name": "User"
}'
```
**Login (Giriş):**
```bash
curl -X POST http://localhost:8000/api/v1/auth/jwt/create/ \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"password": "admin123"
}'
```
**Get User Profile:**
```bash
# Önce login olup token al, sonra:
curl -X GET http://localhost:8000/api/v1/auth/users/me/ \
-H "Authorization: Bearer <your_access_token>"
```
## 📧 Email Testing
### MailPit Kurulumu (Opsiyonel)
```bash
# Docker ile
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit
# Sonra email'leri görüntüle:
# http://localhost:8025
```
**Not:** MailPit olmadan da sistem çalışır, sadece email'ler console'a yazılır.
## 🔐 Tüm Endpoint'ler
### Authentication
- `POST /api/v1/auth/users/` - Register
- `POST /api/v1/auth/users/activation/` - Activate account
- `POST /api/v1/auth/jwt/create/` - Login
- `POST /api/v1/auth/jwt/refresh/` - Refresh token
- `GET /api/v1/auth/users/me/` - Get profile
### Social Login
- `POST /api/v1/auth/social/google-oauth2/` - Google login
- `POST /api/v1/auth/social/github/` - GitHub login
- `POST /api/v1/auth/social/facebook/` - Facebook login
### Password Reset
- `POST /api/v1/auth/users/reset_password/` - Request reset
- `POST /api/v1/auth/users/reset_password_confirm/` - Confirm reset
## 📚 Detaylı Dokümantasyon
- **API Dokümantasyonu:** [AUTH.md](./AUTH.md)
- **Proje Genel Bakış:** [README.md](./README.md)
- **Geliştirme Notları:** [COPILOT_MEMORY.md](./COPILOT_MEMORY.md)
## 🛠️ Sonraki Adımlar
### 1. Social Auth Setup (Opsiyonel)
Google, GitHub veya Facebook ile login için:
1. Provider'dan OAuth credentials al
2. `.env` dosyasına ekle:
```bash
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-client-id
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-client-secret
```
### 2. Frontend Entegrasyonu
- Nuxt.js veya Next.js ile entegre et
- [AUTH.md](./AUTH.md) dosyasında detaylı örnekler var
### 3. Production Deployment
- PostgreSQL database kur
- SMTP email provider ayarla
- Environment variables'ı production için güncelle
- HTTPS enable et
## ✨ Özellikler
- ✅ Email-based authentication (username yok)
- ✅ JWT tokens (60 min access, 7 days refresh)
- ✅ Email activation (register sonrası)
- ✅ Social login (Google, GitHub, Facebook)
- ✅ Password reset
- ✅ Rate limiting (100/hour anon, 1000/hour user)
- ✅ CORS support (SPA için)
- ✅ Modern email templates
- ✅ Admin panel
## 🐛 Sorun Giderme
### Server çalışmıyor?
```bash
# Virtual environment aktif mi kontrol et
source .venv/bin/activate
# Migration'lar uygulandı mı?
python manage.py migrate
# Port 8000 kullanımda mı?
lsof -i :8000
```
### Email gönderilmiyor?
- MailPit çalışıyor mu? `http://localhost:8025`
- Console'da email içeriğini görebilirsin
### JWT token çalışmıyor?
- Token'ın expire olmadığından emin ol (60 dakika)
- Header formatı: `Authorization: Bearer <token>`
## 💡 İpuçları
1. **Development:**
- `DEBUG=True` olmalı
- SQLite database kullan
- MailPit ile email test et
2. **Production:**
- `DEBUG=False` yap
- PostgreSQL kullan
- Gerçek SMTP provider kullan
- HTTPS enable et
3. **Frontend:**
- JWT tokens'ı localStorage veya cookie'de sakla
- Refresh token ile otomatik yenileme yap
- 401 hatalarında login sayfasına yönlendir
## 📞 Yardım
Sorularınız için:
- [AUTH.md](./AUTH.md) - Detaylı API dokümantasyonu
- [README.md](./README.md) - Proje genel bakış
- [COPILOT_MEMORY.md](./COPILOT_MEMORY.md) - Geliştirme notları
---
**Başarılar! 🎉**

323
README.md Normal file
View File

@@ -0,0 +1,323 @@
# Django REST API - Authentication System
Django 6.0 tabanlı, email authentication, JWT tokens ve social login desteği olan modern bir REST API.
## 🚀 Özellikler
-**Email-based Authentication** (username yok)
-**JWT Tokens** (access + refresh)
-**Email Activation** (kayıt sonrası aktivasyon)
-**Social Login** (Google, GitHub, Facebook)
-**Password Reset** (email ile)
-**Rate Limiting** (güvenlik için)
-**CORS Support** (SPA frontend'ler için)
-**Modern Email Templates** (HTML + plain text)
## 📋 Gereksinimler
- Python 3.10+
- Django 6.0
- PostgreSQL (production) veya SQLite (development)
- MailPit (development için email testing)
## 🛠️ Kurulum
### 1. Repository'yi Clone'layın
```bash
git clone <your-repo-url>
cd server
```
### 2. Virtual Environment Oluşturun
```bash
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# veya
.venv\Scripts\activate # Windows
```
### 3. Bağımlılıkları Yükleyin
```bash
pip install -r req.txt
```
### 4. Environment Variables
```bash
cp .env.example .env
# .env dosyasını düzenleyin
```
### 5. Database Migration
```bash
python manage.py migrate
```
### 6. Superuser Oluşturun
```bash
python manage.py createsuperuser
```
### 7. Development Server'ı Başlatın
```bash
python manage.py runserver
```
API: `http://localhost:8000/api/v1/`
Admin: `http://localhost:8000/admin/`
## 📧 Email Testing (MailPit)
Development ortamında email'leri test etmek için MailPit kullanıyoruz.
### MailPit Kurulumu
```bash
# Docker ile
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit
# veya binary ile
# https://github.com/axllent/mailpit/releases
```
### MailPit Web UI
`http://localhost:8025` - Gönderilen email'leri görüntüleyin
## 🔐 Authentication Endpoints
### Register
```bash
POST /api/v1/auth/users/
Content-Type: application/json
{
"email": "user@example.com",
"password": "StrongP@ssw0rd123",
"re_password": "StrongP@ssw0rd123",
"first_name": "Ali",
"last_name": "Veli"
}
```
### Activate Account
```bash
POST /api/v1/auth/users/activation/
Content-Type: application/json
{
"uid": "MQ",
"token": "c4h7vu-..."
}
```
### Login
```bash
POST /api/v1/auth/jwt/create/
Content-Type: application/json
{
"email": "user@example.com",
"password": "StrongP@ssw0rd123"
}
```
### Social Login
```bash
POST /api/v1/auth/social/google-oauth2/
Content-Type: application/json
{
"access_token": "ya29.a0AfH6SMBx..."
}
```
Detaylı API dokümantasyonu için: [AUTH.md](./AUTH.md)
## 🏗️ Proje Yapısı
```
server/
├── accounts/ # Custom user app
│ ├── migrations/
│ ├── models.py # CustomUser model
│ ├── serializers.py # DRF serializers
│ ├── views.py # Social login view
│ ├── admin.py # Admin configuration
│ ├── pipeline.py # Social auth pipeline
│ └── urls.py # URL routing
├── core/ # Project settings
│ ├── settings.py # Main settings
│ ├── urls.py # Root URL config
│ └── wsgi.py
├── templates/
│ └── email/ # Email templates
│ ├── activation_email.html
│ ├── activation_email.txt
│ ├── confirmation_email.html
│ ├── confirmation_email.txt
│ ├── password_reset_email.html
│ └── password_reset_email.txt
├── manage.py
├── req.txt # Python dependencies
├── .env.example # Environment variables template
├── AUTH.md # API documentation
├── COPILOT_MEMORY.md # Development log
└── README.md # This file
```
## 🔧 Konfigürasyon
### Social Auth Setup
#### Google OAuth2
1. [Google Cloud Console](https://console.developers.google.com/) → Create Project
2. APIs & Services → Credentials → Create OAuth 2.0 Client ID
3. Authorized redirect URIs: `http://localhost:8000/api/v1/social/complete/google-oauth2/`
4. `.env` dosyasına ekleyin:
```bash
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-client-id
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-client-secret
```
#### GitHub OAuth
1. [GitHub Settings](https://github.com/settings/developers) → OAuth Apps → New OAuth App
2. Authorization callback URL: `http://localhost:8000/api/v1/social/complete/github/`
3. `.env` dosyasına ekleyin:
```bash
SOCIAL_AUTH_GITHUB_KEY=your-client-id
SOCIAL_AUTH_GITHUB_SECRET=your-client-secret
```
#### Facebook OAuth
1. [Facebook Developers](https://developers.facebook.com/) → Create App
2. Add Facebook Login product
3. Valid OAuth Redirect URIs: `http://localhost:8000/api/v1/social/complete/facebook/`
4. `.env` dosyasına ekleyin:
```bash
SOCIAL_AUTH_FACEBOOK_KEY=your-app-id
SOCIAL_AUTH_FACEBOOK_SECRET=your-app-secret
```
## 🧪 Testing
### Manuel Test
```bash
# Register
curl -X POST http://localhost:8000/api/v1/auth/users/ \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestP@ss123","re_password":"TestP@ss123","first_name":"Test","last_name":"User"}'
# Check MailPit: http://localhost:8025
# Activate (uid ve token email'den alın)
curl -X POST http://localhost:8000/api/v1/auth/users/activation/ \
-H "Content-Type: application/json" \
-d '{"uid":"MQ","token":"c4h7vu-..."}'
# Login
curl -X POST http://localhost:8000/api/v1/auth/jwt/create/ \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestP@ss123"}'
```
### Unit Tests (TODO)
```bash
python manage.py test accounts
```
## 📱 Frontend Entegrasyonu
### Nuxt.js / Next.js
Detaylı entegrasyon örnekleri için [AUTH.md](./AUTH.md) dosyasına bakın.
**Temel Flow:**
1. Frontend'de register form → Backend'e POST
2. Kullanıcı email'ini kontrol eder
3. Aktivasyon linkine tıklar → Frontend yakalayıp backend'e POST
4. Login form → JWT tokens alınır
5. Tokens localStorage/cookie'de saklanır
6. Her request'te `Authorization: Bearer <token>` header'ı eklenir
## 🚀 Production Deployment
### 1. Environment Variables
```bash
DEBUG=False
SECRET_KEY=<strong-random-key>
ALLOWED_HOSTS=yourdomain.com,api.yourdomain.com
# PostgreSQL
DATABASE_URL=postgresql://user:pass@host:5432/dbname
# SMTP Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
# Social Auth Keys
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=...
SOCIAL_AUTH_GITHUB_KEY=...
```
### 2. Security Settings
`settings.py` içinde production için:
```python
DEBUG = False
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
```
### 3. Static Files
```bash
python manage.py collectstatic
```
### 4. Database Migration
```bash
python manage.py migrate
```
### 5. Gunicorn/uWSGI
```bash
gunicorn core.wsgi:application --bind 0.0.0.0:8000
```
## 📚 Dokümantasyon
- **API Documentation:** [AUTH.md](./AUTH.md)
- **Cache Documentation:** [CACHE.md](./CACHE.md)
- **Development Log:** [COPILOT_MEMORY.md](./COPILOT_MEMORY.md)
- **Djoser Docs:** https://djoser.readthedocs.io/
- **SimpleJWT Docs:** https://django-rest-framework-simplejwt.readthedocs.io/
- **Python Social Auth:** https://python-social-auth.readthedocs.io/
## 🤝 Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## 📝 License
This project is licensed under the MIT License.
## 👤 Author
Your Name - [@yourhandle](https://twitter.com/yourhandle)
## 🙏 Acknowledgments
- Django Team
- Django REST Framework
- Djoser
- Python Social Auth
- MailPit
---
**Happy Coding! 🎉**

192
REVIEWS_API.md Normal file
View File

@@ -0,0 +1,192 @@
# Değerlendirme ve Puanlama Sistemi (Reviews API) Dokümantasyonu
Bu doküman, `reviews` uygulaması tarafından sağlanan API endpoint'lerinin frontend tarafında nasıl kullanılacağınııklar. Bu sistem polimorfik bir yapıdadır, yani sadece ürünler için değil, ileride eklenebilecek diğer modüller (Blog, Satıcı vb.) için de kullanılabilir.
## 1. Genel Bilgiler
* **Base URL:** `/api/v1/reviews/`
* **Authentication:** Oy verme işlemi (`POST`) için kullanıcının giriş yapmış olması (Token ile) gerekir. Listeleme (`GET`) işlemi herkese açıktır.
## 2. Ürün Verisinde Puanlama Bilgisi
Ürünleri listelediğinizde (`/api/v1/products/`) veya detayını çektiğinizde (`/api/v1/products/<slug>/`), ürün objesi artık şu iki alanı içerir:
* **`average_rating` (Float):** Ürünün ortalama puanı (1.0 - 5.0 arası). Hiç oy yoksa `0.0` döner.
* **`rating_count` (Integer):** Toplam kaç kişinin oy verdiği.
**Örnek Ürün Objesi:**
```json
{
"id": 15,
"title": "Örnek Ürün",
"price": 100.0,
"average_rating": 4.5, // <-- Ortalama Puan
"rating_count": 12, // <-- Oy Sayısı
"images": "/media/uploads/products/example.avif",
...
}
```
Bu bilgileri ürün kartlarında yıldız göstermek için kullanabilirsiniz.
---
## 3. API Endpoint'leri
### 3.1. Oy Verme ve Yorum Yapma
Kullanıcının bir nesneye (örneğin bir ürüne) puan vermesini ve isteğe bağlı olarak yorum yapmasını sağlar. Bir kullanıcı aynı nesneye sadece bir kez oy verebilir; ikinci istekte mevcut oy güncellenir.
* **URL:** `/api/v1/reviews/rate/`
* **Method:** `POST`
* **Permission:** `IsAuthenticated` (Header'da `Authorization: Bearer <token>` olmalı)
#### İstek (Request) Body Parametreleri:
| Parametre | Tip | Zorunlu | Açıklama |
| :--- | :--- | :--- | :--- |
| `model_name` | String | Evet | Oylanacak nesnenin model adı. Örn: `"product"` |
| `object_id` | Integer | Evet | Oylanacak nesnenin ID'si. Örn: `15` |
| `score` | Integer | Evet | 1 ile 5 arasında bir tam sayı. |
| `comment` | String | Hayır | Kullanıcının yorumu. |
#### Örnek İstek (JSON):
```json
{
"model_name": "product",
"object_id": 15,
"score": 5,
"comment": "Ürün beklediğimden çok daha kaliteli çıktı, tavsiye ederim."
}
```
#### Başarılı Yanıt (201 Created veya 200 OK):
```json
{
"id": 42,
"score": 5,
"comment": "Ürün beklediğimden çok daha kaliteli çıktı, tavsiye ederim.",
"user": "kullanici_adi",
"created_at": "2023-10-27T10:00:00Z",
"model_name": "product",
"object_id": 15
}
```
#### Hata Yanıtları:
* **400 Bad Request:** Eksik parametre veya geçersiz puan (1-5 dışı).
* **401 Unauthorized:** Giriş yapılmamış.
* **404 Not Found:** Belirtilen model veya ID bulunamadı.
---
### 3.2. Değerlendirmeleri Listeleme
Belirli bir nesneye ait tüm yorumları ve puanları listeler. Genellikle ürün detay sayfasının altındaki "Yorumlar" sekmesinde kullanılır.
* **URL:** `/api/v1/reviews/list/`
* **Method:** `GET`
* **Permission:** `AllowAny` (Herkes erişebilir)
#### Query Parametreleri:
| Parametre | Zorunlu | Açıklama |
| :--- | :--- | :--- |
| `model` | Evet | Listelenecek nesnenin model adı. Örn: `product`|
| `id` | Evet | Listelenecek nesnenin ID'si. |
#### Örnek İstek:
`GET /api/v1/reviews/list/?model=product&id=15`
#### Başarılı Yanıt (200 OK):
```json
[
{
"id": 42,
"score": 5,
"comment": "Harika ürün!",
"user": "ahmet123",
"created_at": "2023-10-27T10:00:00Z",
"model_name": "product",
"object_id": 15
},
{
"id": 43,
"score": 4,
"comment": "Kargo biraz geç geldi ama ürün güzel.",
"user": "ayse_yilmaz",
"created_at": "2023-10-26T14:30:00Z",
"model_name": "product",
"object_id": 15
}
]
```
---
## 4. Frontend Entegrasyon Senaryosu (React Örneği)
Bir ürün detay sayfasında (Product Detail Page) yapılması gerekenler:
1. **Ürün Bilgisini Çek:** `/api/v1/products/<slug>/` endpoint'inden ürünü çekin. Gelen `average_rating` ve `rating_count` ile sayfanın üst kısmında yıldızları gösterin.
2. **Yorumları Çek:** Ürün ID'sini kullanarak `/api/v1/reviews/list/?model=product&id=<id>` endpoint'inden yorumları çekin ve aşağıda listeleyin.
```javascript
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function ProductDetail({ slug }) {
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState([]);
useEffect(() => {
async function fetchData() {
// 1. Ürün detayını çek
const prodRes = await axios.get(`/api/v1/products/${slug}/`);
setProduct(prodRes.data);
// 2. Ürünün yorumlarını çek (prodRes.data.id kullanarak)
const reviewRes = await axios.get(`/api/v1/reviews/list/`, {
params: { model: 'product', id: prodRes.data.id }
});
setReviews(reviewRes.data);
}
fetchData();
}, [slug]);
if (!product) return <div>Yükleniyor...</div>;
return (
<div>
<h1>{product.title}</h1>
{/* Özet Puan Gösterimi */}
<div className="rating-summary">
<span>Ortalama Puan: {product.average_rating} / 5</span>
<span>({product.rating_count} değerlendirme)</span>
</div>
{/* ... Ürün detayları ... */}
{/* Yorumlar Listesi */}
<div className="reviews-section">
<h3>Yorumlar</h3>
{reviews.map(review => (
<div key={review.id} className="review-card">
<strong>{review.user}</strong> - <span>{review.score} Yıldız</span>
<p>{review.comment}</p>
</div>
))}
</div>
</div>
);
}
```
## 5. Notlar
* **Resim Yolları:** API'den dönen resim yolları `/media/...` şeklinde olabilir. Frontend tarafında base URL'i (`http://localhost:8000` veya canlı sunucu adresi) başına eklemeniz gerekebilir.

54
SERVICES_README.md Normal file
View File

@@ -0,0 +1,54 @@
# Django ve Celery Servis Scriptleri
Bu scriptler Django ve Celery servislerini kolayca başlatıp durdurmak için kullanılır.
## Kullanım
### Servisleri Başlatma
```bash
./start-services.sh
```
Bu script:
- ✅ Django migrasyonlarını çalıştırır
- ✅ Static dosyaları toplar
- ✅ Django development server'ı başlatır (port 8000)
- ✅ Celery worker'ı başlatır
- ✅ Logları canlı olarak gösterir
### Servisleri Durdurma
**Yöntem 1:** `Ctrl+C` tuşlarına basın (start-services.sh çalışırken)
**Yöntem 2:** Ayrı bir terminal'de:
```bash
./stop-services.sh
```
## Log Dosyaları
Loglar `./logs/` klasöründe saklanır:
- `logs/django.log` - Django sunucusu logları
- `logs/celery.log` - Celery worker logları
## PID Dosyaları
Process ID'leri `./pids/` klasöründe saklanır:
- `pids/django.pid`
- `pids/celery_worker.pid`
## Özellikler
- 🎨 Renkli terminal çıktısı
- 📝 Ayrı log dosyaları
- 🔄 Graceful shutdown (Ctrl+C ile)
- 🧹 Otomatik cleanup
- 🚦 Process takibi
## Notlar
- Development ortamı için tasarlanmıştır
- Production'da Docker veya systemd kullanmanız önerilir
- Port 8000 kullanılmalı (değiştirmek için start-services.sh'i düzenleyin)

239
SOCIAL_AUTH_SETUP.md Normal file
View File

@@ -0,0 +1,239 @@
# 🔐 Social Authentication Setup & Test Guide
## ⚠️ Google OAuth Setup (ZORUNLU)
Google ile login çalışması için callback URL'lerini Google Console'da eklemeniz gerekiyor:
### Adımlar:
1. **Google Cloud Console'a git:**
https://console.cloud.google.com/apis/credentials
2. **Projenizi seçin** (veya yeni proje oluşturun)
3. **OAuth 2.0 Client ID'nize tıklayın:**
- Client ID: `915364976256-691m0s87as2r5vdbqr96f6humblseobt.apps.googleusercontent.com`
4. **"Authorized redirect URIs" bölümüne şu URL'leri ekleyin:**
```
http://localhost:8000/api/v1/social/complete/google-oauth2/
http://localhost:8000/complete/google-oauth2/
http://127.0.0.1:8000/api/v1/social/complete/google-oauth2/
```
5. **"Authorized JavaScript origins" bölümüne:**
```
http://localhost:8000
http://127.0.0.1:8000
```
6. **Kaydet** butonuna tıklayın
---
## 🧪 Test Seçenekleri
### Seçenek 1: HTML Test Sayfası (ÖNERİLEN ✅)
En kolay test yöntemi:
```bash
# Tarayıcınızda açın:
file:///home/beyhan/Python/server/templates/test_social_auth.html
```
**Veya:**
1. Dosya yöneticisinde `/home/beyhan/Python/server/templates/test_social_auth.html` dosyasına git
2. Çift tıkla (tarayıcıda açılır)
3. "Login with Google" butonuna tıkla
---
### Seçenek 2: Python Test Scripti
Terminal'de interaktif test:
```bash
cd /home/beyhan/Python/server
source .venv/bin/activate
python test_social_auth_manual.py
```
Bu script ile:
- Google OAuth test edebilirsiniz (real token ile)
- GitHub OAuth test edebilirsiniz
---
### Seçenek 3: Google OAuth Playground
Gerçek access token almak için:
1. **Git:** https://developers.google.com/oauthplayground/
2. **Settings (sağ üstte ⚙️):**
- "Use your own OAuth credentials" seçeneğini aktif et
- OAuth Client ID: `915364976256-691m0s87as2r5vdbqr96f6humblseobt.apps.googleusercontent.com`
- OAuth Client secret: `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`
3. **Step 1 - Select & authorize APIs:**
- Google OAuth2 API v2 seç
- `https://www.googleapis.com/auth/userinfo.email` ✅
- `https://www.googleapis.com/auth/userinfo.profile` ✅
- "Authorize APIs" butonuna tıkla
4. **Step 2 - Exchange authorization code for tokens:**
- "Exchange authorization code for tokens" butonuna tıkla
- Access token kopyala
5. **Test et:**
```bash
curl -X POST http://localhost:8000/api/v1/auth/social/google-oauth2/ \
-H "Content-Type: application/json" \
-d '{"access_token":"<BURAYA_TOKEN_YAPIŞTIR>"}'
```
---
## 🐙 GitHub OAuth Setup
GitHub için daha basit:
### Adımlar:
1. **GitHub Settings'e git:**
https://github.com/settings/developers
2. **OAuth Apps'e tıkla**
3. **Uygulamanızı bul veya yeni oluştur:**
- Application name: Django REST API Test
- Homepage URL: `http://localhost:8000`
- Authorization callback URL: `http://localhost:8000/api/v1/social/complete/github/`
4. **Client ID ve Client Secret'ı kontrol et:**
- Client ID: `Ov23liUt9B61O46Mdfm4`
- Client Secret: `c7fc8dcb1b2c8f22120608425d07d5efd995baaf`
### Test için Personal Access Token:
Alternatif olarak, test için GitHub Personal Access Token kullanabilirsiniz:
1. **Git:** https://github.com/settings/tokens
2. **Generate new token (classic)**
3. **Scopes seç:**
- `user` ✅
- `user:email` ✅
4. **Token'ı oluştur ve kopyala**
5. **Test et:**
```bash
curl -X POST http://localhost:8000/api/v1/auth/social/github/ \
-H "Content-Type: application/json" \
-d '{"access_token":"<GITHUB_TOKEN>"}'
```
---
## ✅ Test Kontrolü
Başarılı bir social login response'u şöyle görünmeli:
```json
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 2,
"email": "user@gmail.com",
"first_name": "John",
"last_name": "Doe",
"is_active": true,
"date_joined": "2025-12-12T22:00:00Z"
}
}
```
---
## 🐛 Troubleshooting
### "redirect_uri_mismatch" hatası:
- Google Console'da redirect URI'ları kontrol edin
- Tam olarak `http://localhost:8000/api/v1/social/complete/google-oauth2/` olmalı
### "invalid_client" hatası:
- Client ID ve Secret'ın doğru olduğundan emin olun
- Google Console'da OAuth consent screen'i yapılandırdınız mı?
### "Email not provided" hatası:
- OAuth scope'unda `userinfo.email` var mı kontrol edin
- Provider settings'de email scope'u aktif mi?
### Server hatası:
```bash
# Server çalıştığından emin olun:
cd /home/beyhan/Python/server
source .venv/bin/activate
python manage.py runserver
```
### Social auth view çalışmıyor:
```bash
# URL'leri kontrol edin:
curl http://localhost:8000/api/v1/auth/social/google-oauth2/ \
-X POST -H "Content-Type: application/json" \
-d '{"access_token":"test"}'
# 400 Bad Request bekliyoruz (invalid token), 404 değil!
```
---
## 📊 Test Checklist
- [ ] Google Console'da redirect URI'lar eklendi
- [ ] Django server çalışıyor (`python manage.py runserver`)
- [ ] HTML test sayfasıılıyor
- [ ] Google "Login" butonu çalışıyor
- [ ] Access token alınıyor
- [ ] Backend'e request gidiyor
- [ ] JWT tokens dönüyor
- [ ] User bilgileri görünüyor
---
## 🎯 Hızlı Test Komutu
En hızlı test:
```bash
# 1. Server'ı başlat (bir terminalde)
cd /home/beyhan/Python/server
source .venv/bin/activate
python manage.py runserver
# 2. Test scriptini çalıştır (başka terminalde)
cd /home/beyhan/Python/server
source .venv/bin/activate
python test_social_auth_manual.py
```
Veya:
```bash
# HTML sayfasını tarayıcıda aç:
xdg-open /home/beyhan/Python/server/templates/test_social_auth.html
```
---
## 📚 Daha Fazla Bilgi
- **API Dokümantasyonu:** [AUTH.md](./AUTH.md)
- **Genel Bakış:** [README.md](./README.md)
- **Hızlı Başlangıç:** [QUICK_START.md](./QUICK_START.md)
---
**İyi Testler! 🚀**

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', 'date_joined')
list_filter = ('is_staff', 'is_superuser', 'is_active', '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')}),
(_('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', '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'

27
accounts/middleware.py Normal file
View File

@@ -0,0 +1,27 @@
"""
Custom middleware for social authentication.
"""
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

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

103
accounts/models.py Normal file
View File

@@ -0,0 +1,103 @@
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.'
),
)
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

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

3
accounts/tests.py Normal file
View File

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

51
accounts/urls.py 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'),
]

271
accounts/views.py Normal file
View File

@@ -0,0 +1,271 @@
from django.shortcuts import redirect
from django.views import View
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
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 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)

2
backup/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
default_app_config = 'backup.apps.BackupConfig'

368
backup/admin.py Normal file
View File

@@ -0,0 +1,368 @@
from django.contrib import admin
from django.utils.html import format_html
from django.contrib import messages
from django.utils import timezone
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import path, reverse
from django.conf import settings
import os
from datetime import datetime
from .models import DatabaseBackup
from .views import BackupManager
@admin.register(DatabaseBackup)
class DatabaseBackupAdmin(admin.ModelAdmin):
list_display = ['name', 'status_badge', 'backup_type', 'file_size_display', 'download_link', 'created_by', 'created_at', 'completed_at']
list_filter = ['status', 'backup_type', 'created_at']
search_fields = ['name', 'notes', 'error_message']
readonly_fields = ['file_path', 'file_size', 'status', 'created_by', 'created_at', 'completed_at', 'error_message', 'file_size_display_field']
fieldsets = (
('Temel Bilgiler', {
'fields': ('name', 'backup_type', 'status', 'notes')
}),
('Yedek Dosya Bilgileri', {
'fields': ('file_path', 'file_size_display_field')
}),
('Zaman Bilgileri', {
'fields': ('created_by', 'created_at', 'completed_at')
}),
('Hata Bilgileri', {
'fields': ('error_message',),
'classes': ('collapse',)
}),
)
actions = ['create_new_backup', 'restore_selected_backup', 'download_backup', 'delete_backup_files']
def status_badge(self, obj):
"""Durum için renkli badge gösterir"""
colors = {
'pending': '#FFA500',
'in_progress': '#2196F3',
'completed': '#4CAF50',
'failed': '#F44336',
}
color = colors.get(obj.status, '#999')
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 10px; border-radius: 3px; font-weight: bold;">{}</span>',
color,
obj.get_status_display()
)
status_badge.short_description = 'Durum'
def file_size_display(self, obj):
"""Dosya boyutunu gösterir"""
return obj.get_file_size_display()
file_size_display.short_description = 'Dosya Boyutu'
def file_size_display_field(self, obj):
"""Read-only field için dosya boyutu"""
return obj.get_file_size_display()
file_size_display_field.short_description = 'Dosya Boyutu'
def download_link(self, obj):
"""İndir butonu gösterir"""
if obj.file_path and obj.status == 'completed' and os.path.isfile(obj.file_path):
url = f'/admin/backup/databasebackup/{obj.pk}/download/'
return format_html(
'<a href="{}" class="button" style="background-color: #4CAF50; color: white; padding: 5px 10px; '
'text-decoration: none; border-radius: 3px; display: inline-block;">📥 İndir</a>',
url
)
return format_html('<span style="color: {};">-</span>', '#999')
download_link.short_description = 'İndir'
def get_urls(self):
"""Admin için özel URL'ler ekler"""
urls = super().get_urls()
custom_urls = [
path('create-backup/', self.admin_site.admin_view(self.create_backup_view), name='backup_create'),
path('upload-backup/', self.admin_site.admin_view(self.upload_backup_view), name='backup_upload'),
path('<int:backup_id>/download/', self.admin_site.admin_view(self.download_backup_file), name='backup_download'),
]
return custom_urls + urls
def changelist_view(self, request, extra_context=None):
"""Change list view'a ekstra context ekler"""
extra_context = extra_context or {}
extra_context['show_create_backup_button'] = True
extra_context['show_upload_backup_button'] = True
return super().changelist_view(request, extra_context=extra_context)
def create_backup_view(self, request):
"""Yeni yedek oluşturma view'i"""
from django.shortcuts import redirect
from django.urls import reverse
# Yeni bir backup objesi oluştur
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
backup = DatabaseBackup.objects.create(
name=f"Manuel Yedek - {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}",
backup_type='manual',
created_by=request.user,
status='pending'
)
# Yedekleme işlemini başlat
manager = BackupManager()
success, message = manager.create_backup(backup)
if success:
self.message_user(request, message, messages.SUCCESS)
else:
self.message_user(request, message, messages.ERROR)
# Liste sayfasına yönlendir
return redirect(reverse('admin:backup_databasebackup_changelist'))
def upload_backup_view(self, request):
"""Yedek dosyası yükleme view'i"""
if request.method == 'POST':
uploaded_file = request.FILES.get('backup_file')
backup_name = request.POST.get('backup_name', '')
if not uploaded_file:
self.message_user(request, "Lütfen bir dosya seçin", messages.ERROR)
return redirect(reverse('admin:backup_upload'))
# Dosya uzantısı kontrolü
if not uploaded_file.name.endswith('.sql'):
self.message_user(request, "Sadece .sql uzantılı dosyalar yüklenebilir", messages.ERROR)
return redirect(reverse('admin:backup_upload'))
# Dosya boyutu kontrolü (max 500MB)
max_size = 500 * 1024 * 1024 # 500MB in bytes
if uploaded_file.size > max_size:
self.message_user(request, "Dosya çok büyük. Maksimum 500MB olabilir", messages.ERROR)
return redirect(reverse('admin:backup_upload'))
try:
# Backup klasörünü kontrol et
manager = BackupManager()
backup_dir = manager.backup_dir
# Dosya adını oluştur (timestamp ekle)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
original_name = os.path.splitext(uploaded_file.name)[0]
filename = f"uploaded_{original_name}_{timestamp}.sql"
file_path = os.path.join(backup_dir, filename)
# Dosyayı kaydet
with open(file_path, 'wb+') as destination:
for chunk in uploaded_file.chunks():
destination.write(chunk)
# Veritabanı kaydı oluştur
if not backup_name:
backup_name = f"Yüklenen Yedek - {uploaded_file.name}"
backup = DatabaseBackup.objects.create(
name=backup_name,
file_path=file_path,
file_size=uploaded_file.size,
status='completed',
backup_type='manual',
created_by=request.user,
completed_at=timezone.now(),
notes=f"Dosya yüklendi: {uploaded_file.name}"
)
self.message_user(
request,
f"Yedek dosyası başarıyla yüklendi: {uploaded_file.name} ({backup.get_file_size_display()})",
messages.SUCCESS
)
return redirect(reverse('admin:backup_databasebackup_changelist'))
except Exception as e:
self.message_user(request, f"Dosya yüklenirken hata oluştu: {str(e)}", messages.ERROR)
return redirect(reverse('admin:backup_upload'))
# GET request - form göster
context = {
**self.admin_site.each_context(request),
'title': 'Yedek Dosyası Yükle',
'opts': self.model._meta,
'has_view_permission': self.has_view_permission(request),
}
return render(request, 'admin/backup/upload_backup.html', context)
def download_backup_file(self, request, backup_id):
"""Yedek dosyasını indirir"""
backup = get_object_or_404(DatabaseBackup, pk=backup_id)
if not backup.file_path:
self.message_user(request, "Yedek dosyası bulunamadı", messages.ERROR)
return HttpResponse("Dosya bulunamadı", status=404)
if not os.path.isfile(backup.file_path):
self.message_user(request, "Yedek dosyası disk üzerinde bulunamadı", messages.ERROR)
return HttpResponse("Dosya disk üzerinde bulunamadı", status=404)
# Dosyayı indir
try:
response = FileResponse(open(backup.file_path, 'rb'), content_type='application/sql')
filename = os.path.basename(backup.file_path)
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
except Exception as e:
return HttpResponse(f"Dosya indirilemedi: {str(e)}", status=500)
def save_model(self, request, obj, form, change):
"""Model kaydedilirken created_by alanını otomatik doldur"""
if not change: # Yeni kayıt
obj.created_by = request.user
super().save_model(request, obj, form, change)
def create_new_backup(self, request, queryset):
"""Yeni bir yedek oluşturur"""
# Yeni bir backup objesi oluştur
timestamp = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
backup = DatabaseBackup.objects.create(
name=f"Manuel Yedek - {timestamp}",
backup_type='manual',
created_by=request.user,
status='pending'
)
# Yedekleme işlemini başlat
manager = BackupManager()
success, message = manager.create_backup(backup)
if success:
self.message_user(request, message, messages.SUCCESS)
else:
self.message_user(request, message, messages.ERROR)
create_new_backup.short_description = "Yeni Yedek Oluştur"
def restore_selected_backup(self, request, queryset):
"""Seçili yedeği geri yükler"""
if queryset.count() != 1:
self.message_user(
request,
"Lütfen geri yüklemek için sadece bir yedek seçin",
messages.WARNING
)
return
backup = queryset.first()
if backup.status != 'completed':
self.message_user(
request,
"Sadece tamamlanmış yedekler geri yüklenebilir",
messages.WARNING
)
return
if not backup.file_path:
self.message_user(
request,
"Yedek dosya yolu bulunamadı",
messages.ERROR
)
return
# Restore işlemi (migration'lar da dahil)
manager = BackupManager()
success, message = manager.restore_backup(backup.file_path)
if success:
# Otomatik migration çalıştır
try:
from django.core.management import call_command
import io
call_command('migrate', '--noinput', stdout=io.StringIO(), stderr=io.StringIO())
self.message_user(request, f"{message} Migration'lar uygulandı. Sayfayı yenileyin.", messages.SUCCESS)
except:
self.message_user(request, f"{message} Sayfayı yenileyin.", messages.SUCCESS)
else:
self.message_user(request, message, messages.ERROR)
restore_selected_backup.short_description = "Seçili Yedeği Geri Yükle"
def download_backup(self, request, queryset):
"""Seçili yedeği indirir"""
if queryset.count() != 1:
self.message_user(
request,
"Lütfen indirmek için sadece bir yedek seçin",
messages.WARNING
)
return
backup = queryset.first()
if backup.status != 'completed':
self.message_user(
request,
"Sadece tamamlanmış yedekler indirilebilir",
messages.WARNING
)
return
if not backup.file_path or not os.path.isfile(backup.file_path):
self.message_user(
request,
"Yedek dosyası bulunamadı",
messages.ERROR
)
return
# Dosyayı indir
try:
response = FileResponse(open(backup.file_path, 'rb'), content_type='application/sql')
filename = os.path.basename(backup.file_path)
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
except Exception as e:
self.message_user(
request,
f"Dosya indirilemedi: {str(e)}",
messages.ERROR
)
return
download_backup.short_description = "Seçili Yedeği İndir"
def delete_backup_files(self, request, queryset):
"""Seçili yedeklerin dosyalarını siler"""
deleted_count = 0
error_count = 0
manager = BackupManager()
for backup in queryset:
if backup.file_path:
success, message = manager.delete_backup_file(backup.file_path)
if success:
backup.file_path = None
backup.file_size = None
backup.save()
deleted_count += 1
else:
error_count += 1
if deleted_count > 0:
self.message_user(
request,
f"{deleted_count} yedek dosyası silindi",
messages.SUCCESS
)
if error_count > 0:
self.message_user(
request,
f"{error_count} yedek dosyası silinemedi",
messages.WARNING
)
delete_backup_files.short_description = "Yedek Dosyalarını Sil"
def has_delete_permission(self, request, obj=None):
"""Silme iznini kontrol et - Tüm admin kullanıcıları silebilir"""
return request.user.is_staff

10
backup/apps.py Normal file
View File

@@ -0,0 +1,10 @@
from django.apps import AppConfig
class BackupConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'backup'
def ready(self):
"""Uygulama hazır olduğunda sinyalleri import et"""
import backup.models # Sinyalleri kaydetmek için import et

View File

@@ -0,0 +1,38 @@
# Generated by Django 6.0 on 2025-12-22 16:52
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='DatabaseBackup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Yedek Adı')),
('file_path', models.CharField(blank=True, max_length=500, null=True, verbose_name='Dosya Yolu')),
('file_size', models.BigIntegerField(blank=True, null=True, verbose_name='Dosya Boyutu (bytes)')),
('status', models.CharField(choices=[('pending', 'Bekliyor'), ('in_progress', 'İşleniyor'), ('completed', 'Tamamlandı'), ('failed', 'Başarısız')], default='pending', max_length=20, verbose_name='Durum')),
('backup_type', models.CharField(choices=[('manual', 'Manuel'), ('automatic', 'Otomatik')], default='manual', max_length=20, verbose_name='Yedek Tipi')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Tamamlanma Tarihi')),
('error_message', models.TextField(blank=True, null=True, verbose_name='Hata Mesajı')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Oluşturan')),
],
options={
'verbose_name': 'Veritabanı Yedeği',
'verbose_name_plural': 'Veritabanı Yedekleri',
'ordering': ['-created_at'],
},
),
]

View File

68
backup/models.py Normal file
View File

@@ -0,0 +1,68 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.db.models.signals import post_delete
from django.dispatch import receiver
import os
User = get_user_model()
class DatabaseBackup(models.Model):
"""Veritabanı yedekleme kayıtlarını tutar"""
STATUS_CHOICES = [
('pending', 'Bekliyor'),
('in_progress', 'İşleniyor'),
('completed', 'Tamamlandı'),
('failed', 'Başarısız'),
]
BACKUP_TYPE_CHOICES = [
('manual', 'Manuel'),
('automatic', 'Otomatik'),
]
name = models.CharField(max_length=255, verbose_name='Yedek Adı')
file_path = models.CharField(max_length=500, verbose_name='Dosya Yolu', blank=True, null=True)
file_size = models.BigIntegerField(verbose_name='Dosya Boyutu (bytes)', null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='Durum')
backup_type = models.CharField(max_length=20, choices=BACKUP_TYPE_CHOICES, default='manual', verbose_name='Yedek Tipi')
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Oluşturan')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')
completed_at = models.DateTimeField(null=True, blank=True, verbose_name='Tamamlanma Tarihi')
error_message = models.TextField(blank=True, null=True, verbose_name='Hata Mesajı')
notes = models.TextField(blank=True, null=True, verbose_name='Notlar')
class Meta:
verbose_name = 'Veritabanı Yedeği'
verbose_name_plural = 'Veritabanı Yedekleri'
ordering = ['-created_at']
def __str__(self):
return f"{self.name} - {self.get_status_display()}"
def get_file_size_display(self):
"""Dosya boyutunu okunabilir formatta döndürür"""
if not self.file_size:
return "N/A"
size = self.file_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} TB"
@receiver(post_delete, sender=DatabaseBackup)
def delete_backup_file(sender, instance, **kwargs):
"""
Backup kaydı silindiğinde, ilişkili fiziksel dosyayı da siler
"""
if instance.file_path and os.path.isfile(instance.file_path):
try:
os.remove(instance.file_path)
print(f"Yedek dosyası silindi: {instance.file_path}")
except Exception as e:
print(f"Dosya silinirken hata oluştu: {e}")

View File

@@ -0,0 +1,21 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static %}
{% block object-tools-items %}
{{ block.super }}
{% if show_create_backup_button %}
<li>
<a href="{% url 'admin:backup_create' %}" class="addlink" style="background-color: #4CAF50; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;">
🔄 Yeni Yedek Al
</a>
</li>
{% endif %}
{% if show_upload_backup_button %}
<li>
<a href="{% url 'admin:backup_upload' %}" class="addlink" style="background-color: #2196F3; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;">
📤 Yedek Yükle
</a>
</li>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,174 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrahead %}
{{ block.super }}
<style>
.upload-form-container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.form-group input[type="text"],
.form-group input[type="file"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input[type="file"] {
padding: 8px;
}
.help-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
}
.btn-container {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background-color: #2196F3;
color: white;
}
.btn-primary:hover {
background-color: #1976D2;
}
.btn-secondary {
background-color: #757575;
color: white;
}
.btn-secondary:hover {
background-color: #616161;
}
.info-box {
background-color: #E3F2FD;
border-left: 4px solid #2196F3;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.info-box h4 {
margin-top: 0;
color: #1976D2;
}
.info-box ul {
margin: 10px 0;
padding-left: 20px;
}
</style>
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:backup_databasebackup_changelist' %}">Veritabanı Yedekleri</a>
&rsaquo; Yedek Yükle
</div>
{% endblock %}
{% block content %}
<div class="upload-form-container">
<h1>📤 Yedek Dosyası Yükle</h1>
<div class="info-box">
<h4> Bilgilendirme</h4>
<ul>
<li>Sadece <strong>.sql</strong> uzantılı dosyalar yüklenebilir</li>
<li>Maksimum dosya boyutu: <strong>500 MB</strong></li>
<li>Yüklenen dosya <code>backups/</code> klasörüne kaydedilecektir</li>
<li>Dosya otomatik olarak timestamp ile adlandırılacaktır</li>
</ul>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="backup_name">Yedek Adı:</label>
<input type="text"
id="backup_name"
name="backup_name"
placeholder="Örn: Production Yedek - 2024-12-24"
maxlength="255">
<span class="help-text">
Boş bırakılırsa dosya adı kullanılacaktır
</span>
</div>
<div class="form-group">
<label for="backup_file">Yedek Dosyası: *</label>
<input type="file"
id="backup_file"
name="backup_file"
accept=".sql"
required>
<span class="help-text">
PostgreSQL SQL dump dosyası (.sql)
</span>
</div>
<div class="btn-container">
<button type="submit" class="btn btn-primary">
📤 Yükle
</button>
<a href="{% url 'admin:backup_databasebackup_changelist' %}" class="btn btn-secondary">
❌ İptal
</a>
</div>
</form>
</div>
<script>
// Dosya seçildiğinde boyut kontrolü
document.getElementById('backup_file').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const maxSize = 500 * 1024 * 1024; // 500MB
if (file.size > maxSize) {
alert('Dosya çok büyük! Maksimum 500MB olabilir.');
e.target.value = '';
return;
}
// Dosya adını backup_name alanına otomatik doldur (eğer boşsa)
const nameField = document.getElementById('backup_name');
if (!nameField.value) {
const fileName = file.name.replace('.sql', '');
nameField.value = 'Yüklenen Yedek - ' + fileName;
}
// Dosya boyutunu göster
const sizeInMB = (file.size / (1024 * 1024)).toFixed(2);
console.log('Dosya boyutu: ' + sizeInMB + ' MB');
}
});
</script>
{% endblock %}

3
backup/tests.py Normal file
View File

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

328
backup/views.py Normal file
View File

@@ -0,0 +1,328 @@
import os
from datetime import datetime
from django.conf import settings
from django.utils import timezone
from .models import DatabaseBackup
try:
import psycopg2
from psycopg2 import sql
PSYCOPG2_AVAILABLE = True
except ImportError:
PSYCOPG2_AVAILABLE = False
class BackupManager:
"""PostgreSQL veritabanı yedekleme işlemlerini yönetir - Sadece psycopg2 kullanarak"""
def __init__(self):
self.backup_dir = os.path.join(settings.BASE_DIR, 'backups')
if not os.path.exists(self.backup_dir):
os.makedirs(self.backup_dir)
def get_db_config(self):
"""Veritabanı yapılandırmasını alır"""
db_config = settings.DATABASES['default']
return {
'dbname': db_config.get('NAME'),
'user': db_config.get('USER'),
'password': db_config.get('PASSWORD'),
'host': db_config.get('HOST', 'localhost'),
'port': db_config.get('PORT', '5432'),
}
def get_connection(self):
"""PostgreSQL bağlantısı oluşturur"""
if not PSYCOPG2_AVAILABLE:
raise Exception("psycopg2 kütüphanesi yüklü değil")
db_config = self.get_db_config()
return psycopg2.connect(
dbname=db_config['dbname'],
user=db_config['user'],
password=db_config['password'],
host=db_config['host'],
port=db_config['port']
)
#@task
def create_backup(self, backup_obj):
"""
PostgreSQL veritabanının yedeğini oluşturur
Sadece psycopg2 kullanarak SQL dump oluşturur
"""
try:
backup_obj.status = 'in_progress'
backup_obj.save()
db_config = self.get_db_config()
# Yedek dosyası adını oluştur
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_filename = f"backup_{db_config['dbname']}_{timestamp}.sql"
backup_path = os.path.join(self.backup_dir, backup_filename)
# Veritabanına bağlan
conn = self.get_connection()
cursor = conn.cursor()
with open(backup_path, 'w', encoding='utf-8') as f:
# Header
f.write("-- PostgreSQL Database Backup\n")
f.write(f"-- Database: {db_config['dbname']}\n")
f.write(f"-- Date: {datetime.now()}\n")
f.write("-- Created by Django Backup System using psycopg2\n\n")
f.write("SET client_encoding = 'UTF8';\n")
f.write("SET standard_conforming_strings = on;\n")
f.write("SET check_function_bodies = false;\n")
f.write("SET client_min_messages = warning;\n\n")
# Tüm tabloları al
cursor.execute("""
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
""")
tables = cursor.fetchall()
for (table_name,) in tables:
f.write(f"\n-- Table: {table_name}\n")
# Tablo yapısını al - kolon bilgilerini çek
cursor.execute("""
SELECT
column_name,
data_type,
character_maximum_length,
is_nullable,
column_default,
is_identity
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = %s
ORDER BY ordinal_position;
""", [table_name])
columns_info = cursor.fetchall()
if columns_info:
f.write(f"DROP TABLE IF EXISTS \"{table_name}\" CASCADE;\n")
f.write(f"CREATE TABLE \"{table_name}\" (\n")
col_defs = []
for col_name, data_type, max_length, is_nullable, col_default, is_identity in columns_info:
col_def = f" \"{col_name}\" "
# Serial kontrolü (Nextval veya Identity)
is_serial = False
if (col_default and 'nextval' in col_default) or is_identity == 'YES':
if data_type == 'integer':
col_def += "SERIAL"
is_serial = True
elif data_type == 'bigint':
col_def += "BIGSERIAL"
is_serial = True
if not is_serial:
# Veri tipini ekle
if max_length and data_type == 'character varying':
col_def += f"VARCHAR({max_length})"
elif max_length and data_type == 'character':
col_def += f"CHAR({max_length})"
else:
col_def += data_type.upper()
# NOT NULL
if is_nullable == 'NO':
col_def += " NOT NULL"
# DEFAULT değer
if col_default:
col_def += f" DEFAULT {col_default}"
col_defs.append(col_def)
f.write(",\n".join(col_defs))
f.write("\n);\n\n")
# Veriyi al ve INSERT komutları oluştur
# Kolon isimlerini al
cursor.execute(sql.SQL("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = %s
ORDER BY ordinal_position;
"""), [table_name])
columns = [row[0] for row in cursor.fetchall()]
if not columns:
continue
cursor.execute(sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name)))
rows = cursor.fetchall()
if rows:
f.write(f"-- Data for table: {table_name}\n")
# INSERT şablonu hazırla
cols_str = ', '.join([f'"{c}"' for c in columns]) # Identifier quoting
placeholders = ', '.join(['%s'] * len(columns))
insert_template = f"INSERT INTO \"{table_name}\" ({cols_str}) VALUES ({placeholders})"
for row in rows:
# mogrify kullanarak güvenli SQL oluştur
try:
# mogrify bytes döndürür, decode etmemiz lazım
safe_sql = cursor.mogrify(insert_template, row).decode('utf-8')
f.write(f"{safe_sql};\n")
except Exception as row_err:
print(f"Row error in {table_name}: {row_err}")
continue
f.write("\n")
# Sequence'leri sıfırla
f.write("\n-- Reset sequences\n")
cursor.execute("""
SELECT
c.relname as sequence_name,
t.relname as table_name,
a.attname as column_name
FROM pg_class c
JOIN pg_depend d ON d.objid = c.oid
JOIN pg_class t ON d.refobjid = t.oid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
WHERE c.relkind = 'S'
AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public');
""")
sequences = cursor.fetchall()
for seq_name, tbl_name, col_name in sequences:
f.write(f"SELECT setval('{seq_name}', (SELECT COALESCE(MAX(\"{col_name}\"), 1) FROM \"{tbl_name}\"));\n")
cursor.close()
conn.close()
# Başarılı
file_size = os.path.getsize(backup_path)
backup_obj.file_path = backup_path
backup_obj.file_size = file_size
backup_obj.status = 'completed'
backup_obj.completed_at = timezone.now()
backup_obj.save()
return True, f"Yedekleme başarıyla tamamlandı: {backup_filename}"
except Exception as e:
backup_obj.status = 'failed'
backup_obj.error_message = str(e)
backup_obj.save()
return False, f"Yedekleme hatası: {str(e)}"
def restore_backup(self, backup_path):
"""
TAMAMEN OTOMATIK FULL RESTORE
Manuel işlem gerektirmez!
"""
try:
if not os.path.exists(backup_path):
return False, "Yedek dosyası bulunamadı"
with open(backup_path, 'r', encoding='utf-8') as f:
sql_content = f.read()
# HOTFIX 1: 'order' gibi keywordlerin tırnak içine alınmaması sorununu düzelt
import re
sql_content = re.sub(r'(\s+)order(\s+[A-Z]+)', r'\1"order"\2', sql_content)
# HOTFIX 2: SERIAL/Sequence düzeltmesi
# "id INTEGER NOT NULL DEFAULT nextval(...)" -> "id SERIAL"
sql_content = re.sub(r'INTEGER\s+NOT\s+NULL\s+DEFAULT\s+nextval\(\'[^\']+\'(:?::regclass)?\)', 'SERIAL', sql_content)
sql_content = re.sub(r'BIGINT\s+NOT\s+NULL\s+DEFAULT\s+nextval\(\'[^\']+\'(:?::regclass)?\)', 'BIGSERIAL', sql_content)
# HOTFIX 3: "id" kolonları INTEGER/BIGINT NOT NULL ise (ve default yoksa) SERIAL yap
# Bu durum Identity kolonlarının yanlış yedeklenmesi sonucu oluşur
sql_content = re.sub(r'"id"\s+INTEGER\s+NOT\s+NULL(?!(\s+DEFAULT))', '"id" SERIAL', sql_content)
sql_content = re.sub(r'"id"\s+BIGINT\s+NOT\s+NULL(?!(\s+DEFAULT))', '"id" BIGSERIAL', sql_content)
# HOTFIX 4: setval satırlarını kaldır (çünkü biz kendimiz yeniden ayarlıyoruz ve isimler değişmiş olabilir)
# Lines starting with SELECT setval...
sql_lines = []
for line in sql_content.split('\n'):
if 'SELECT setval' in line and 'django_migrations' in line or 'SELECT setval' in line:
continue # Skip setvals from file
sql_lines.append(line)
sql_content = '\n'.join(sql_lines)
conn = self.get_connection()
conn.autocommit = True
cursor = conn.cursor()
try:
print("=" * 60)
print("TAM OTOMATIK RESTORE (YENI VERSIYON)")
print("=" * 60)
# 1. DROP tüm tablolar
print("\n1. Temizleniyor...")
cursor.execute("SELECT tablename FROM pg_tables WHERE schemaname = 'public';")
tables = cursor.fetchall()
for (t,) in tables:
print(f" Dropping {t}...")
cursor.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE;')
print(" ✓ Temizlendi")
# 2. SQL Execution - Tek seferde çalıştır
print("\n2. SQL Dosyası Çalıştırılıyor...")
# execute() methodu çoklu sorguları çalıştırabilir (psycopg2 özelliği)
try:
cursor.execute(sql_content)
print(" ✓ SQL Script çalıştırıldı")
except Exception as sql_err:
print(f" SQL HATA: {sql_err}")
raise sql_err
print(" ✓ SQL Script çalıştırıldı")
# 3. Sequence'ler (SQL script içinde genelde vardır ama garanti olsun)
print("\n3. Sequence'ler Kontrol Ediliyor...")
cursor.execute("""
SELECT c.relname, t.relname, a.attname
FROM pg_class c
JOIN pg_depend d ON d.objid = c.oid
JOIN pg_class t ON d.refobjid = t.oid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
WHERE c.relkind = 'S' AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public');
""")
for seq, tbl, col in cursor.fetchall():
try:
cursor.execute(f"SELECT setval('{seq}', COALESCE((SELECT MAX({col}) FROM {tbl}), 1));")
except:
pass
print(" ✓ Ayarlandı")
cursor.close()
conn.close()
print("\n" + "=" * 60)
print("RESTORE TAMAMLANDI!")
print("=" * 60)
return True, "Restore başarıyla tamamlandı!"
except Exception as e:
print(f"\nHATA: {e}")
cursor.close()
conn.close()
raise
except Exception as e:
return False, f"Geri yükleme hatası: {str(e)}"
def delete_backup_file(self, backup_path):
"""Yedek dosyasını fiziksel olarak siler"""
try:
if os.path.exists(backup_path):
os.remove(backup_path)
return True, "Yedek dosyası silindi"
else:
return False, "Yedek dosyası bulunamadı"
except Exception as e:
return False, f"Dosya silme hatası: {str(e)}"

View File

@@ -0,0 +1,818 @@
-- PostgreSQL Database Backup
-- Database: shop
-- Date: 2026-01-19 01:29:47.426406
-- Created by Django Backup System using psycopg2
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
-- Table: accounts_customuser
DROP TABLE IF EXISTS accounts_customuser CASCADE;
CREATE TABLE accounts_customuser (
id BIGINT NOT NULL,
password VARCHAR(128) NOT NULL,
last_login TIMESTAMP WITH TIME ZONE,
is_superuser BOOLEAN NOT NULL,
email VARCHAR(254) NOT NULL,
first_name VARCHAR(150) NOT NULL,
last_name VARCHAR(150) NOT NULL,
is_staff BOOLEAN NOT NULL,
is_active BOOLEAN NOT NULL,
date_joined TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Data for table: accounts_customuser
INSERT INTO accounts_customuser (id, password, last_login, is_superuser, email, first_name, last_name, is_staff, is_active, date_joined) VALUES (1, 'pbkdf2_sha256$1200000$X8eAbh3gvzNupPRIIc1RI4$SjIgdHr0UTX5IO0pMY89BBiqxOLzLly3BG/1ZkEPuR4=', '2026-01-18 22:28:08.286360+00:00', True, 'beyhan@beyhan.dev', '', '', True, True, '2026-01-18 22:27:20.666592+00:00');
-- Table: accounts_customuser_groups
DROP TABLE IF EXISTS accounts_customuser_groups CASCADE;
CREATE TABLE accounts_customuser_groups (
id BIGINT NOT NULL,
customuser_id BIGINT NOT NULL,
group_id INTEGER NOT NULL
);
-- Table: accounts_customuser_user_permissions
DROP TABLE IF EXISTS accounts_customuser_user_permissions CASCADE;
CREATE TABLE accounts_customuser_user_permissions (
id BIGINT NOT NULL,
customuser_id BIGINT NOT NULL,
permission_id INTEGER NOT NULL
);
-- Table: auth_group
DROP TABLE IF EXISTS auth_group CASCADE;
CREATE TABLE auth_group (
id INTEGER NOT NULL,
name VARCHAR(150) NOT NULL
);
-- Table: auth_group_permissions
DROP TABLE IF EXISTS auth_group_permissions CASCADE;
CREATE TABLE auth_group_permissions (
id BIGINT NOT NULL,
group_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL
);
-- Table: auth_permission
DROP TABLE IF EXISTS auth_permission CASCADE;
CREATE TABLE auth_permission (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
content_type_id INTEGER NOT NULL,
codename VARCHAR(100) NOT NULL
);
-- Data for table: auth_permission
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (1, 'Can add log entry', 1, 'add_logentry');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (2, 'Can change log entry', 1, 'change_logentry');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (3, 'Can delete log entry', 1, 'delete_logentry');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (4, 'Can view log entry', 1, 'view_logentry');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (5, 'Can add permission', 3, 'add_permission');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (6, 'Can change permission', 3, 'change_permission');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (7, 'Can delete permission', 3, 'delete_permission');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (8, 'Can view permission', 3, 'view_permission');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (9, 'Can add group', 2, 'add_group');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (10, 'Can change group', 2, 'change_group');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (11, 'Can delete group', 2, 'delete_group');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (12, 'Can view group', 2, 'view_group');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (13, 'Can add content type', 4, 'add_contenttype');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (14, 'Can change content type', 4, 'change_contenttype');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (15, 'Can delete content type', 4, 'delete_contenttype');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (16, 'Can view content type', 4, 'view_contenttype');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (17, 'Can add session', 5, 'add_session');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (18, 'Can change session', 5, 'change_session');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (19, 'Can delete session', 5, 'delete_session');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (20, 'Can view session', 5, 'view_session');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (21, 'Can add task result', 8, 'add_taskresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (22, 'Can change task result', 8, 'change_taskresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (23, 'Can delete task result', 8, 'delete_taskresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (24, 'Can view task result', 8, 'view_taskresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (25, 'Can add chord counter', 6, 'add_chordcounter');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (26, 'Can change chord counter', 6, 'change_chordcounter');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (27, 'Can delete chord counter', 6, 'delete_chordcounter');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (28, 'Can view chord counter', 6, 'view_chordcounter');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (29, 'Can add group result', 7, 'add_groupresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (30, 'Can change group result', 7, 'change_groupresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (31, 'Can delete group result', 7, 'delete_groupresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (32, 'Can view group result', 7, 'view_groupresult');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (33, 'Can add crontab', 10, 'add_crontabschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (34, 'Can change crontab', 10, 'change_crontabschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (35, 'Can delete crontab', 10, 'delete_crontabschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (36, 'Can view crontab', 10, 'view_crontabschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (37, 'Can add interval', 11, 'add_intervalschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (38, 'Can change interval', 11, 'change_intervalschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (39, 'Can delete interval', 11, 'delete_intervalschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (40, 'Can view interval', 11, 'view_intervalschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (41, 'Can add periodic task', 12, 'add_periodictask');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (42, 'Can change periodic task', 12, 'change_periodictask');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (43, 'Can delete periodic task', 12, 'delete_periodictask');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (44, 'Can view periodic task', 12, 'view_periodictask');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (45, 'Can add periodic tasks', 13, 'add_periodictasks');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (46, 'Can change periodic tasks', 13, 'change_periodictasks');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (47, 'Can delete periodic tasks', 13, 'delete_periodictasks');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (48, 'Can view periodic tasks', 13, 'view_periodictasks');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (49, 'Can add solar event', 14, 'add_solarschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (50, 'Can change solar event', 14, 'change_solarschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (51, 'Can delete solar event', 14, 'delete_solarschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (52, 'Can view solar event', 14, 'view_solarschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (53, 'Can add clocked', 9, 'add_clockedschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (54, 'Can change clocked', 9, 'change_clockedschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (55, 'Can delete clocked', 9, 'delete_clockedschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (56, 'Can view clocked', 9, 'view_clockedschedule');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (57, 'Can add association', 15, 'add_association');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (58, 'Can change association', 15, 'change_association');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (59, 'Can delete association', 15, 'delete_association');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (60, 'Can view association', 15, 'view_association');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (61, 'Can add code', 16, 'add_code');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (62, 'Can change code', 16, 'change_code');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (63, 'Can delete code', 16, 'delete_code');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (64, 'Can view code', 16, 'view_code');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (65, 'Can add nonce', 17, 'add_nonce');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (66, 'Can change nonce', 17, 'change_nonce');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (67, 'Can delete nonce', 17, 'delete_nonce');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (68, 'Can view nonce', 17, 'view_nonce');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (69, 'Can add user social auth', 19, 'add_usersocialauth');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (70, 'Can change user social auth', 19, 'change_usersocialauth');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (71, 'Can delete user social auth', 19, 'delete_usersocialauth');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (72, 'Can view user social auth', 19, 'view_usersocialauth');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (73, 'Can add partial', 18, 'add_partial');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (74, 'Can change partial', 18, 'change_partial');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (75, 'Can delete partial', 18, 'delete_partial');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (76, 'Can view partial', 18, 'view_partial');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (77, 'Can add site', 20, 'add_site');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (78, 'Can change site', 20, 'change_site');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (79, 'Can delete site', 20, 'delete_site');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (80, 'Can view site', 20, 'view_site');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (81, 'Can add user', 21, 'add_customuser');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (82, 'Can change user', 21, 'change_customuser');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (83, 'Can delete user', 21, 'delete_customuser');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (84, 'Can view user', 21, 'view_customuser');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (85, 'Can add Banner', 22, 'add_banner');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (86, 'Can change Banner', 22, 'change_banner');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (87, 'Can delete Banner', 22, 'delete_banner');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (88, 'Can view Banner', 22, 'view_banner');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (89, 'Can add Site Ayarı', 23, 'add_setting');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (90, 'Can change Site Ayarı', 23, 'change_setting');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (91, 'Can delete Site Ayarı', 23, 'delete_setting');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (92, 'Can view Site Ayarı', 23, 'view_setting');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (93, 'Can add Site Ayarı Aç / Kapat', 24, 'add_sitesettings');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (94, 'Can change Site Ayarı Aç / Kapat', 24, 'change_sitesettings');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (95, 'Can delete Site Ayarı Aç / Kapat', 24, 'delete_sitesettings');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (96, 'Can view Site Ayarı Aç / Kapat', 24, 'view_sitesettings');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (97, 'Can add Veritabanı Yedeği', 25, 'add_databasebackup');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (98, 'Can change Veritabanı Yedeği', 25, 'change_databasebackup');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (99, 'Can delete Veritabanı Yedeği', 25, 'delete_databasebackup');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (100, 'Can view Veritabanı Yedeği', 25, 'view_databasebackup');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (101, 'Can add Contact', 26, 'add_contact');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (102, 'Can change Contact', 26, 'change_contact');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (103, 'Can delete Contact', 26, 'delete_contact');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (104, 'Can view Contact', 26, 'view_contact');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (105, 'Can add Galeri Resmi', 29, 'add_images');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (106, 'Can change Galeri Resmi', 29, 'change_images');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (107, 'Can delete Galeri Resmi', 29, 'delete_images');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (108, 'Can view Galeri Resmi', 29, 'view_images');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (109, 'Can add Ürün Kategori', 27, 'add_category');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (110, 'Can change Ürün Kategori', 27, 'change_category');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (111, 'Can delete Ürün Kategori', 27, 'delete_category');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (112, 'Can view Ürün Kategori', 27, 'view_category');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (113, 'Can add Ürün Tagı', 31, 'add_tags');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (114, 'Can change Ürün Tagı', 31, 'change_tags');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (115, 'Can delete Ürün Tagı', 31, 'delete_tags');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (116, 'Can view Ürün Tagı', 31, 'view_tags');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (117, 'Can add Ürün', 30, 'add_product');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (118, 'Can change Ürün', 30, 'change_product');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (119, 'Can delete Ürün', 30, 'delete_product');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (120, 'Can view Ürün', 30, 'view_product');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (121, 'Can add Ürüm Yorum', 28, 'add_comment');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (122, 'Can change Ürüm Yorum', 28, 'change_comment');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (123, 'Can delete Ürüm Yorum', 28, 'delete_comment');
INSERT INTO auth_permission (id, name, content_type_id, codename) VALUES (124, 'Can view Ürüm Yorum', 28, 'view_comment');
-- Table: backup_databasebackup
DROP TABLE IF EXISTS backup_databasebackup CASCADE;
CREATE TABLE backup_databasebackup (
id BIGINT NOT NULL,
name VARCHAR(255) NOT NULL,
file_path VARCHAR(500),
file_size BIGINT,
status VARCHAR(20) NOT NULL,
backup_type VARCHAR(20) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
completed_at TIMESTAMP WITH TIME ZONE,
error_message TEXT,
notes TEXT,
created_by_id BIGINT
);
-- Data for table: backup_databasebackup
INSERT INTO backup_databasebackup (id, name, file_path, file_size, status, backup_type, created_at, completed_at, error_message, notes, created_by_id) VALUES (1, 'aaa', NULL, NULL, 'pending', 'manual', '2026-01-18 22:29:33.971861+00:00', NULL, NULL, '', 1);
INSERT INTO backup_databasebackup (id, name, file_path, file_size, status, backup_type, created_at, completed_at, error_message, notes, created_by_id) VALUES (2, 'Manuel Yedek - 2026-01-18 22:29:47', NULL, NULL, 'in_progress', 'manual', '2026-01-18 22:29:47.223287+00:00', NULL, NULL, NULL, 1);
-- Table: banners
DROP TABLE IF EXISTS banners CASCADE;
CREATE TABLE banners (
id BIGINT NOT NULL,
color VARCHAR(25) NOT NULL,
title VARCHAR(254),
text1 VARCHAR(254),
text2 VARCHAR(254),
text4 VARCHAR(254),
text5 VARCHAR(254),
image VARCHAR(100) NOT NULL,
image_k VARCHAR(100),
image_k_txt VARCHAR(254),
is_active BOOLEAN NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: categories
DROP TABLE IF EXISTS categories CASCADE;
CREATE TABLE categories (
id BIGINT NOT NULL,
title VARCHAR(254) NOT NULL,
keywords VARCHAR(254) NOT NULL,
description VARCHAR(254) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN NOT NULL,
order INTEGER NOT NULL,
slug VARCHAR(250) NOT NULL,
parent_id BIGINT,
images VARCHAR(100)
);
-- Table: comments
DROP TABLE IF EXISTS comments CASCADE;
CREATE TABLE comments (
id BIGINT NOT NULL,
title VARCHAR(254) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN NOT NULL,
slug VARCHAR(50) NOT NULL,
parent_id BIGINT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL
);
-- Table: contacts
DROP TABLE IF EXISTS contacts CASCADE;
CREATE TABLE contacts (
id BIGINT NOT NULL,
name VARCHAR(254) NOT NULL,
email VARCHAR(254) NOT NULL,
subject VARCHAR(254) NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
user_id BIGINT,
ip VARCHAR(100)
);
-- Table: django_admin_log
DROP TABLE IF EXISTS django_admin_log CASCADE;
CREATE TABLE django_admin_log (
id INTEGER NOT NULL,
action_time TIMESTAMP WITH TIME ZONE NOT NULL,
object_id TEXT,
object_repr VARCHAR(200) NOT NULL,
action_flag SMALLINT NOT NULL,
change_message TEXT NOT NULL,
content_type_id INTEGER,
user_id BIGINT NOT NULL
);
-- Data for table: django_admin_log
INSERT INTO django_admin_log (id, action_time, object_id, object_repr, action_flag, change_message, content_type_id, user_id) VALUES (1, '2026-01-18 22:29:34.026717+00:00', '1', 'aaa - Bekliyor', 1, '[{"added": {}}]', 25, 1);
-- Table: django_celery_beat_clockedschedule
DROP TABLE IF EXISTS django_celery_beat_clockedschedule CASCADE;
CREATE TABLE django_celery_beat_clockedschedule (
id BIGINT NOT NULL,
clocked_time TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: django_celery_beat_crontabschedule
DROP TABLE IF EXISTS django_celery_beat_crontabschedule CASCADE;
CREATE TABLE django_celery_beat_crontabschedule (
id BIGINT NOT NULL,
minute VARCHAR(240) NOT NULL,
hour VARCHAR(96) NOT NULL,
day_of_week VARCHAR(64) NOT NULL,
day_of_month VARCHAR(124) NOT NULL,
month_of_year VARCHAR(64) NOT NULL,
timezone VARCHAR(63) NOT NULL
);
-- Table: django_celery_beat_intervalschedule
DROP TABLE IF EXISTS django_celery_beat_intervalschedule CASCADE;
CREATE TABLE django_celery_beat_intervalschedule (
id BIGINT NOT NULL,
every INTEGER NOT NULL,
period VARCHAR(24) NOT NULL
);
-- Table: django_celery_beat_periodictask
DROP TABLE IF EXISTS django_celery_beat_periodictask CASCADE;
CREATE TABLE django_celery_beat_periodictask (
id BIGINT NOT NULL,
name VARCHAR(200) NOT NULL,
task VARCHAR(200) NOT NULL,
args TEXT NOT NULL,
kwargs TEXT NOT NULL,
queue VARCHAR(200),
exchange VARCHAR(200),
routing_key VARCHAR(200),
expires TIMESTAMP WITH TIME ZONE,
enabled BOOLEAN NOT NULL,
last_run_at TIMESTAMP WITH TIME ZONE,
total_run_count INTEGER NOT NULL,
date_changed TIMESTAMP WITH TIME ZONE NOT NULL,
description TEXT NOT NULL,
crontab_id BIGINT,
interval_id BIGINT,
solar_id BIGINT,
one_off BOOLEAN NOT NULL,
start_time TIMESTAMP WITH TIME ZONE,
priority INTEGER,
headers TEXT NOT NULL,
clocked_id BIGINT,
expire_seconds INTEGER
);
-- Table: django_celery_beat_periodictasks
DROP TABLE IF EXISTS django_celery_beat_periodictasks CASCADE;
CREATE TABLE django_celery_beat_periodictasks (
ident SMALLINT NOT NULL,
last_update TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: django_celery_beat_solarschedule
DROP TABLE IF EXISTS django_celery_beat_solarschedule CASCADE;
CREATE TABLE django_celery_beat_solarschedule (
id BIGINT NOT NULL,
event VARCHAR(24) NOT NULL,
latitude NUMERIC NOT NULL,
longitude NUMERIC NOT NULL
);
-- Table: django_celery_results_chordcounter
DROP TABLE IF EXISTS django_celery_results_chordcounter CASCADE;
CREATE TABLE django_celery_results_chordcounter (
id INTEGER NOT NULL,
group_id VARCHAR(255) NOT NULL,
sub_tasks TEXT NOT NULL,
count INTEGER NOT NULL
);
-- Table: django_celery_results_groupresult
DROP TABLE IF EXISTS django_celery_results_groupresult CASCADE;
CREATE TABLE django_celery_results_groupresult (
id INTEGER NOT NULL,
group_id VARCHAR(255) NOT NULL,
date_created TIMESTAMP WITH TIME ZONE NOT NULL,
date_done TIMESTAMP WITH TIME ZONE NOT NULL,
content_type VARCHAR(128) NOT NULL,
content_encoding VARCHAR(64) NOT NULL,
result TEXT
);
-- Table: django_celery_results_taskresult
DROP TABLE IF EXISTS django_celery_results_taskresult CASCADE;
CREATE TABLE django_celery_results_taskresult (
id INTEGER NOT NULL,
task_id VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL,
content_type VARCHAR(128) NOT NULL,
content_encoding VARCHAR(64) NOT NULL,
result TEXT,
date_done TIMESTAMP WITH TIME ZONE NOT NULL,
traceback TEXT,
meta TEXT,
task_args TEXT,
task_kwargs TEXT,
task_name VARCHAR(255),
worker VARCHAR(100),
date_created TIMESTAMP WITH TIME ZONE NOT NULL,
periodic_task_name VARCHAR(255),
date_started TIMESTAMP WITH TIME ZONE
);
-- Table: django_content_type
DROP TABLE IF EXISTS django_content_type CASCADE;
CREATE TABLE django_content_type (
id INTEGER NOT NULL,
app_label VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL
);
-- Data for table: django_content_type
INSERT INTO django_content_type (id, app_label, model) VALUES (1, 'admin', 'logentry');
INSERT INTO django_content_type (id, app_label, model) VALUES (2, 'auth', 'group');
INSERT INTO django_content_type (id, app_label, model) VALUES (3, 'auth', 'permission');
INSERT INTO django_content_type (id, app_label, model) VALUES (4, 'contenttypes', 'contenttype');
INSERT INTO django_content_type (id, app_label, model) VALUES (5, 'sessions', 'session');
INSERT INTO django_content_type (id, app_label, model) VALUES (6, 'django_celery_results', 'chordcounter');
INSERT INTO django_content_type (id, app_label, model) VALUES (7, 'django_celery_results', 'groupresult');
INSERT INTO django_content_type (id, app_label, model) VALUES (8, 'django_celery_results', 'taskresult');
INSERT INTO django_content_type (id, app_label, model) VALUES (9, 'django_celery_beat', 'clockedschedule');
INSERT INTO django_content_type (id, app_label, model) VALUES (10, 'django_celery_beat', 'crontabschedule');
INSERT INTO django_content_type (id, app_label, model) VALUES (11, 'django_celery_beat', 'intervalschedule');
INSERT INTO django_content_type (id, app_label, model) VALUES (12, 'django_celery_beat', 'periodictask');
INSERT INTO django_content_type (id, app_label, model) VALUES (13, 'django_celery_beat', 'periodictasks');
INSERT INTO django_content_type (id, app_label, model) VALUES (14, 'django_celery_beat', 'solarschedule');
INSERT INTO django_content_type (id, app_label, model) VALUES (15, 'social_django', 'association');
INSERT INTO django_content_type (id, app_label, model) VALUES (16, 'social_django', 'code');
INSERT INTO django_content_type (id, app_label, model) VALUES (17, 'social_django', 'nonce');
INSERT INTO django_content_type (id, app_label, model) VALUES (18, 'social_django', 'partial');
INSERT INTO django_content_type (id, app_label, model) VALUES (19, 'social_django', 'usersocialauth');
INSERT INTO django_content_type (id, app_label, model) VALUES (20, 'sites', 'site');
INSERT INTO django_content_type (id, app_label, model) VALUES (21, 'accounts', 'customuser');
INSERT INTO django_content_type (id, app_label, model) VALUES (22, 'settings', 'banner');
INSERT INTO django_content_type (id, app_label, model) VALUES (23, 'settings', 'setting');
INSERT INTO django_content_type (id, app_label, model) VALUES (24, 'settings', 'sitesettings');
INSERT INTO django_content_type (id, app_label, model) VALUES (25, 'backup', 'databasebackup');
INSERT INTO django_content_type (id, app_label, model) VALUES (26, 'contact', 'contact');
INSERT INTO django_content_type (id, app_label, model) VALUES (27, 'product', 'category');
INSERT INTO django_content_type (id, app_label, model) VALUES (28, 'product', 'comment');
INSERT INTO django_content_type (id, app_label, model) VALUES (29, 'product', 'images');
INSERT INTO django_content_type (id, app_label, model) VALUES (30, 'product', 'product');
INSERT INTO django_content_type (id, app_label, model) VALUES (31, 'product', 'tags');
-- Table: django_migrations
DROP TABLE IF EXISTS django_migrations CASCADE;
CREATE TABLE django_migrations (
id BIGINT NOT NULL,
app VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
applied TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Data for table: django_migrations
INSERT INTO django_migrations (id, app, name, applied) VALUES (1, 'contenttypes', '0001_initial', '2026-01-18 22:26:26.197648+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (2, 'contenttypes', '0002_remove_content_type_name', '2026-01-18 22:26:26.360052+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (3, 'auth', '0001_initial', '2026-01-18 22:26:26.904480+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (4, 'auth', '0002_alter_permission_name_max_length', '2026-01-18 22:26:27.004323+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (5, 'auth', '0003_alter_user_email_max_length', '2026-01-18 22:26:27.071923+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (6, 'auth', '0004_alter_user_username_opts', '2026-01-18 22:26:27.167428+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (7, 'auth', '0005_alter_user_last_login_null', '2026-01-18 22:26:27.303966+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (8, 'auth', '0006_require_contenttypes_0002', '2026-01-18 22:26:27.391310+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (9, 'auth', '0007_alter_validators_add_error_messages', '2026-01-18 22:26:27.484909+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (10, 'auth', '0008_alter_user_username_max_length', '2026-01-18 22:26:27.577593+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (11, 'auth', '0009_alter_user_last_name_max_length', '2026-01-18 22:26:27.774582+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (12, 'auth', '0010_alter_group_name_max_length', '2026-01-18 22:26:27.931659+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (13, 'auth', '0011_update_proxy_permissions', '2026-01-18 22:26:27.999110+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (14, 'auth', '0012_alter_user_first_name_max_length', '2026-01-18 22:26:28.091754+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (15, 'accounts', '0001_initial', '2026-01-18 22:26:28.718278+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (16, 'admin', '0001_initial', '2026-01-18 22:26:28.997267+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (17, 'admin', '0002_logentry_remove_auto_add', '2026-01-18 22:26:29.035111+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (18, 'admin', '0003_logentry_add_action_flag_choices', '2026-01-18 22:26:29.132077+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (19, 'backup', '0001_initial', '2026-01-18 22:26:29.396877+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (20, 'contact', '0001_initial', '2026-01-18 22:26:29.498738+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (21, 'contact', '0002_contact_user', '2026-01-18 22:26:29.726625+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (22, 'contact', '0003_alter_contact_message', '2026-01-18 22:26:29.828197+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (23, 'contact', '0004_contact_ip', '2026-01-18 22:26:29.992327+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (24, 'contact', '0005_alter_contact_ip_alter_contact_user', '2026-01-18 22:26:30.323272+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (25, 'django_celery_beat', '0001_initial', '2026-01-18 22:26:30.768960+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (26, 'django_celery_beat', '0002_auto_20161118_0346', '2026-01-18 22:26:30.969010+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (27, 'django_celery_beat', '0003_auto_20161209_0049', '2026-01-18 22:26:31.072305+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (28, 'django_celery_beat', '0004_auto_20170221_0000', '2026-01-18 22:26:31.133956+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (29, 'django_celery_beat', '0005_add_solarschedule_events_choices', '2026-01-18 22:26:31.224248+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (30, 'django_celery_beat', '0006_auto_20180322_0932', '2026-01-18 22:26:31.519108+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (31, 'django_celery_beat', '0007_auto_20180521_0826', '2026-01-18 22:26:31.718953+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (32, 'django_celery_beat', '0008_auto_20180914_1922', '2026-01-18 22:26:31.798080+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (33, 'django_celery_beat', '0006_auto_20180210_1226', '2026-01-18 22:26:31.898867+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (34, 'django_celery_beat', '0006_periodictask_priority', '2026-01-18 22:26:32.054824+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (35, 'django_celery_beat', '0009_periodictask_headers', '2026-01-18 22:26:32.212981+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (36, 'django_celery_beat', '0010_auto_20190429_0326', '2026-01-18 22:26:32.426848+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (37, 'django_celery_beat', '0011_auto_20190508_0153', '2026-01-18 22:26:32.685772+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (38, 'django_celery_beat', '0012_periodictask_expire_seconds', '2026-01-18 22:26:32.783946+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (39, 'django_celery_beat', '0013_auto_20200609_0727', '2026-01-18 22:26:32.849509+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (40, 'django_celery_beat', '0014_remove_clockedschedule_enabled', '2026-01-18 22:26:33.003032+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (41, 'django_celery_beat', '0015_alter_clockedschedule_id_alter_crontabschedule_id_and_more', '2026-01-18 22:26:34.475445+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (42, 'django_celery_results', '0001_initial', '2026-01-18 22:26:34.772179+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (43, 'django_celery_results', '0002_add_task_name_args_kwargs', '2026-01-18 22:26:34.929787+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (44, 'django_celery_results', '0003_auto_20181106_1101', '2026-01-18 22:26:34.992086+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (45, 'django_celery_results', '0004_auto_20190516_0412', '2026-01-18 22:26:35.300946+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (46, 'django_celery_results', '0005_taskresult_worker', '2026-01-18 22:26:35.512788+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (47, 'django_celery_results', '0006_taskresult_date_created', '2026-01-18 22:26:35.748405+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (48, 'django_celery_results', '0007_remove_taskresult_hidden', '2026-01-18 22:26:35.842453+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (49, 'django_celery_results', '0008_chordcounter', '2026-01-18 22:26:36.040012+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (50, 'django_celery_results', '0009_groupresult', '2026-01-18 22:26:37.044478+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (51, 'django_celery_results', '0010_remove_duplicate_indices', '2026-01-18 22:26:37.197820+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (52, 'django_celery_results', '0011_taskresult_periodic_task_name', '2026-01-18 22:26:37.324394+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (53, 'django_celery_results', '0012_taskresult_date_started', '2026-01-18 22:26:37.447071+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (54, 'django_celery_results', '0013_taskresult_django_cele_periodi_1993cf_idx', '2026-01-18 22:26:37.567406+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (55, 'django_celery_results', '0014_alter_taskresult_status', '2026-01-18 22:26:37.630094+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (56, 'product', '0001_initial', '2026-01-18 22:26:39.186225+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (57, 'product', '0002_product_kd_price_alter_product_price', '2026-01-18 22:26:39.293780+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (58, 'product', '0003_rename_kd_price_product_kg_price', '2026-01-18 22:26:39.423004+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (59, 'product', '0004_alter_images_options_alter_images_images_and_more', '2026-01-18 22:26:39.681525+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (60, 'product', '0005_product_special_product_special_images', '2026-01-18 22:26:39.882839+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (61, 'product', '0006_product_thumbnail', '2026-01-18 22:26:40.010784+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (62, 'product', '0007_category_images', '2026-01-18 22:26:40.166425+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (63, 'product', '0008_remove_product_special_images', '2026-01-18 22:26:40.292882+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (64, 'product', '0009_remove_product_special', '2026-01-18 22:26:40.419665+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (65, 'product', '0010_alter_product_price', '2026-01-18 22:26:40.488409+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (66, 'product', '0011_remove_product_kg_price_product_brim', '2026-01-18 22:26:40.715387+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (67, 'product', '0012_alter_product_brim', '2026-01-18 22:26:40.783285+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (68, 'product', '0013_alter_product_brim', '2026-01-18 22:26:40.879267+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (69, 'product', '0014_alter_product_slug', '2026-01-18 22:26:40.975299+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (70, 'product', '0015_alter_product_brim', '2026-01-18 22:26:41.102201+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (71, 'product', '0016_product_gallery', '2026-01-18 22:26:41.516596+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (72, 'product', '0017_alter_product_brim', '2026-01-18 22:26:41.556929+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (73, 'product', '0018_product_is_front', '2026-01-18 22:26:41.750191+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (74, 'product', '0019_product_thumb', '2026-01-18 22:26:41.910670+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (75, 'product', '0020_remove_product_thumbnail_alter_product_thumb', '2026-01-18 22:26:42.051322+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (76, 'product', '0021_remove_product_thumb_alter_category_images_and_more', '2026-01-18 22:26:42.322768+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (77, 'sessions', '0001_initial', '2026-01-18 22:26:42.539586+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (78, 'settings', '0001_initial', '2026-01-18 22:26:42.693943+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (79, 'settings', '0002_setting_linkedin_setting_pinterest', '2026-01-18 22:26:42.914953+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (80, 'settings', '0003_sitesettings', '2026-01-18 22:26:43.040738+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (81, 'settings', '0004_alter_sitesettings_options_sitesettings_site_active', '2026-01-18 22:26:43.197700+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (82, 'settings', '0005_alter_sitesettings_site_active', '2026-01-18 22:26:43.259407+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (83, 'sites', '0001_initial', '2026-01-18 22:26:43.415004+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (84, 'sites', '0002_alter_domain_unique', '2026-01-18 22:26:43.566011+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (85, 'default', '0001_initial', '2026-01-18 22:26:44.103081+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (86, 'social_auth', '0001_initial', '2026-01-18 22:26:44.133067+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (87, 'social_django', '0001_initial', '2026-01-18 22:26:44.162641+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (88, 'default', '0002_add_related_name', '2026-01-18 22:26:44.211596+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (89, 'social_auth', '0002_add_related_name', '2026-01-18 22:26:44.268276+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (90, 'social_django', '0002_add_related_name', '2026-01-18 22:26:44.297551+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (91, 'default', '0003_alter_email_max_length', '2026-01-18 22:26:44.421917+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (92, 'social_auth', '0003_alter_email_max_length', '2026-01-18 22:26:44.451711+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (93, 'social_django', '0003_alter_email_max_length', '2026-01-18 22:26:44.481836+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (94, 'default', '0004_auto_20160423_0400', '2026-01-18 22:26:44.553779+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (95, 'social_auth', '0004_auto_20160423_0400', '2026-01-18 22:26:44.613238+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (96, 'social_django', '0004_auto_20160423_0400', '2026-01-18 22:26:44.643021+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (97, 'social_auth', '0005_auto_20160727_2333', '2026-01-18 22:26:44.765049+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (98, 'social_django', '0005_auto_20160727_2333', '2026-01-18 22:26:44.795770+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (99, 'social_django', '0006_partial', '2026-01-18 22:26:45.017004+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (100, 'social_django', '0007_code_timestamp', '2026-01-18 22:26:45.203222+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (101, 'social_django', '0008_partial_timestamp', '2026-01-18 22:26:45.390796+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (102, 'social_django', '0009_auto_20191118_0520', '2026-01-18 22:26:45.606051+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (103, 'social_django', '0010_uid_db_index', '2026-01-18 22:26:45.768091+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (104, 'social_django', '0011_alter_id_fields', '2026-01-18 22:26:46.483286+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (105, 'social_django', '0012_usersocialauth_extra_data_new', '2026-01-18 22:26:46.719674+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (106, 'social_django', '0013_migrate_extra_data', '2026-01-18 22:26:46.965822+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (107, 'social_django', '0014_remove_usersocialauth_extra_data', '2026-01-18 22:26:47.134938+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (108, 'social_django', '0015_rename_extra_data_new_usersocialauth_extra_data', '2026-01-18 22:26:47.297024+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (109, 'social_django', '0016_alter_usersocialauth_extra_data', '2026-01-18 22:26:47.369579+00:00');
INSERT INTO django_migrations (id, app, name, applied) VALUES (110, 'social_django', '0017_usersocialauth_user_social_auth_uid_required', '2026-01-18 22:26:47.531898+00:00');
-- Table: django_session
DROP TABLE IF EXISTS django_session CASCADE;
CREATE TABLE django_session (
session_key VARCHAR(40) NOT NULL,
session_data TEXT NOT NULL,
expire_date TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Data for table: django_session
INSERT INTO django_session (session_key, session_data, expire_date) VALUES ('tcqskuvq8ylqqsnym8li5ryz5msuh07l', '.eJxVjDsOwjAQRO_iGln-x6GkzxmstXeNA8iR4qRC3B1HSgHdaN6bebMA-1bC3mgNM7Irk-zy20VIT6oHwAfU-8LTUrd1jvxQ-Ekbnxak1-10_w4KtNLXlLMln1COkI0atHMWRp81AYERpIX1yigXM9IAHnvWVngw0nSuyLPPF_siOAc:1vhbFg:OLstq7hX4uJnLrchGxLbzykgqQqPzsfUd738yJCTISA', '2026-02-01 22:28:08.313123+00:00');
-- Table: django_site
DROP TABLE IF EXISTS django_site CASCADE;
CREATE TABLE django_site (
id INTEGER NOT NULL,
domain VARCHAR(100) NOT NULL,
name VARCHAR(50) NOT NULL
);
-- Data for table: django_site
INSERT INTO django_site (id, domain, name) VALUES (1, 'example.com', 'example.com');
-- Table: images
DROP TABLE IF EXISTS images CASCADE;
CREATE TABLE images (
id BIGINT NOT NULL,
title VARCHAR(254) NOT NULL,
is_active BOOLEAN NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
images VARCHAR(100)
);
-- Table: products
DROP TABLE IF EXISTS products CASCADE;
CREATE TABLE products (
id BIGINT NOT NULL,
title VARCHAR(254) NOT NULL,
content TEXT,
keywords VARCHAR(254) NOT NULL,
price DOUBLE PRECISION NOT NULL,
video VARCHAR(254),
slug VARCHAR(250) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN NOT NULL,
images VARCHAR(100),
brim VARCHAR(10) NOT NULL,
is_front BOOLEAN NOT NULL
);
-- Table: products_categories
DROP TABLE IF EXISTS products_categories CASCADE;
CREATE TABLE products_categories (
id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
category_id BIGINT NOT NULL
);
-- Table: products_gallery
DROP TABLE IF EXISTS products_gallery CASCADE;
CREATE TABLE products_gallery (
id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
images_id BIGINT NOT NULL
);
-- Table: products_tags
DROP TABLE IF EXISTS products_tags CASCADE;
CREATE TABLE products_tags (
id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
tags_id BIGINT NOT NULL
);
-- Table: settings
DROP TABLE IF EXISTS settings CASCADE;
CREATE TABLE settings (
id BIGINT NOT NULL,
title VARCHAR(254) NOT NULL,
meta_title VARCHAR(254) NOT NULL,
meta_description VARCHAR(254) NOT NULL,
phone VARCHAR(254) NOT NULL,
url VARCHAR(254),
email VARCHAR(254) NOT NULL,
facebook VARCHAR(254),
x VARCHAR(254),
instagram VARCHAR(254),
whatsapp VARCHAR(254),
slogan VARCHAR(254),
w_logo VARCHAR(100),
b_logo VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN NOT NULL,
linkedin VARCHAR(254),
pinterest VARCHAR(254)
);
-- Table: site_settings
DROP TABLE IF EXISTS site_settings CASCADE;
CREATE TABLE site_settings (
id BIGINT NOT NULL,
is_active BOOLEAN NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
site_active BOOLEAN NOT NULL
);
-- Table: social_auth_association
DROP TABLE IF EXISTS social_auth_association CASCADE;
CREATE TABLE social_auth_association (
id BIGINT NOT NULL,
server_url VARCHAR(255) NOT NULL,
handle VARCHAR(255) NOT NULL,
secret VARCHAR(255) NOT NULL,
issued INTEGER NOT NULL,
lifetime INTEGER NOT NULL,
assoc_type VARCHAR(64) NOT NULL
);
-- Table: social_auth_code
DROP TABLE IF EXISTS social_auth_code CASCADE;
CREATE TABLE social_auth_code (
id BIGINT NOT NULL,
email VARCHAR(254) NOT NULL,
code VARCHAR(32) NOT NULL,
verified BOOLEAN NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: social_auth_nonce
DROP TABLE IF EXISTS social_auth_nonce CASCADE;
CREATE TABLE social_auth_nonce (
id BIGINT NOT NULL,
server_url VARCHAR(255) NOT NULL,
timestamp INTEGER NOT NULL,
salt VARCHAR(65) NOT NULL
);
-- Table: social_auth_partial
DROP TABLE IF EXISTS social_auth_partial CASCADE;
CREATE TABLE social_auth_partial (
id BIGINT NOT NULL,
token VARCHAR(32) NOT NULL,
next_step SMALLINT NOT NULL,
backend VARCHAR(32) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
data JSONB NOT NULL
);
-- Table: social_auth_usersocialauth
DROP TABLE IF EXISTS social_auth_usersocialauth CASCADE;
CREATE TABLE social_auth_usersocialauth (
id BIGINT NOT NULL,
provider VARCHAR(32) NOT NULL,
uid VARCHAR(255) NOT NULL,
user_id BIGINT NOT NULL,
created TIMESTAMP WITH TIME ZONE NOT NULL,
modified TIMESTAMP WITH TIME ZONE NOT NULL,
extra_data JSONB NOT NULL
);
-- Table: tags
DROP TABLE IF EXISTS tags CASCADE;
CREATE TABLE tags (
id BIGINT NOT NULL,
tag VARCHAR(254) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN NOT NULL,
slug VARCHAR(50) NOT NULL
);
-- Reset sequences
SELECT setval('django_migrations_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_migrations));
SELECT setval('django_content_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_content_type));
SELECT setval('auth_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM auth_permission));
SELECT setval('auth_group_id_seq', (SELECT COALESCE(MAX(id), 1) FROM auth_group));
SELECT setval('auth_group_permissions_id_seq', (SELECT COALESCE(MAX(id), 1) FROM auth_group_permissions));
SELECT setval('accounts_customuser_id_seq', (SELECT COALESCE(MAX(id), 1) FROM accounts_customuser));
SELECT setval('accounts_customuser_groups_id_seq', (SELECT COALESCE(MAX(id), 1) FROM accounts_customuser_groups));
SELECT setval('accounts_customuser_user_permissions_id_seq', (SELECT COALESCE(MAX(id), 1) FROM accounts_customuser_user_permissions));
SELECT setval('django_admin_log_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_admin_log));
SELECT setval('backup_databasebackup_id_seq', (SELECT COALESCE(MAX(id), 1) FROM backup_databasebackup));
SELECT setval('contacts_id_seq', (SELECT COALESCE(MAX(id), 1) FROM contacts));
SELECT setval('django_celery_beat_crontabschedule_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_beat_crontabschedule));
SELECT setval('django_celery_beat_intervalschedule_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_beat_intervalschedule));
SELECT setval('django_celery_beat_periodictask_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_beat_periodictask));
SELECT setval('django_celery_beat_solarschedule_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_beat_solarschedule));
SELECT setval('django_celery_beat_clockedschedule_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_beat_clockedschedule));
SELECT setval('django_celery_results_taskresult_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_results_taskresult));
SELECT setval('django_celery_results_chordcounter_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_results_chordcounter));
SELECT setval('django_celery_results_groupresult_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_celery_results_groupresult));
SELECT setval('images_id_seq', (SELECT COALESCE(MAX(id), 1) FROM images));
SELECT setval('categories_id_seq', (SELECT COALESCE(MAX(id), 1) FROM categories));
SELECT setval('tags_id_seq', (SELECT COALESCE(MAX(id), 1) FROM tags));
SELECT setval('products_id_seq', (SELECT COALESCE(MAX(id), 1) FROM products));
SELECT setval('products_categories_id_seq', (SELECT COALESCE(MAX(id), 1) FROM products_categories));
SELECT setval('products_tags_id_seq', (SELECT COALESCE(MAX(id), 1) FROM products_tags));
SELECT setval('comments_id_seq', (SELECT COALESCE(MAX(id), 1) FROM comments));
SELECT setval('products_gallery_id_seq', (SELECT COALESCE(MAX(id), 1) FROM products_gallery));
SELECT setval('banners_id_seq', (SELECT COALESCE(MAX(id), 1) FROM banners));
SELECT setval('settings_id_seq', (SELECT COALESCE(MAX(id), 1) FROM settings));
SELECT setval('site_settings_id_seq', (SELECT COALESCE(MAX(id), 1) FROM site_settings));
SELECT setval('django_site_id_seq', (SELECT COALESCE(MAX(id), 1) FROM django_site));
SELECT setval('social_auth_association_id_seq', (SELECT COALESCE(MAX(id), 1) FROM social_auth_association));
SELECT setval('social_auth_code_id_seq', (SELECT COALESCE(MAX(id), 1) FROM social_auth_code));
SELECT setval('social_auth_nonce_id_seq', (SELECT COALESCE(MAX(id), 1) FROM social_auth_nonce));
SELECT setval('social_auth_usersocialauth_id_seq', (SELECT COALESCE(MAX(id), 1) FROM social_auth_usersocialauth));
SELECT setval('social_auth_partial_id_seq', (SELECT COALESCE(MAX(id), 1) FROM social_auth_partial));

View File

@@ -0,0 +1,819 @@
-- PostgreSQL Database Backup
-- Database: shop
-- Date: 2026-01-19 01:33:37.572514
-- Created by Django Backup System using psycopg2
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
-- Table: accounts_customuser
DROP TABLE IF EXISTS "accounts_customuser" CASCADE;
CREATE TABLE "accounts_customuser" (
"id" BIGSERIAL,
"password" VARCHAR(128) NOT NULL,
"last_login" TIMESTAMP WITH TIME ZONE,
"is_superuser" BOOLEAN NOT NULL,
"email" VARCHAR(254) NOT NULL,
"first_name" VARCHAR(150) NOT NULL,
"last_name" VARCHAR(150) NOT NULL,
"is_staff" BOOLEAN NOT NULL,
"is_active" BOOLEAN NOT NULL,
"date_joined" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Data for table: accounts_customuser
INSERT INTO "accounts_customuser" ("id", "password", "last_login", "is_superuser", "email", "first_name", "last_name", "is_staff", "is_active", "date_joined") VALUES (1, 'pbkdf2_sha256$1200000$X8eAbh3gvzNupPRIIc1RI4$SjIgdHr0UTX5IO0pMY89BBiqxOLzLly3BG/1ZkEPuR4=', '2026-01-18T22:28:08.286360+00:00'::timestamptz, true, 'beyhan@beyhan.dev', '', '', true, true, '2026-01-18T22:27:20.666592+00:00'::timestamptz);
-- Table: accounts_customuser_groups
DROP TABLE IF EXISTS "accounts_customuser_groups" CASCADE;
CREATE TABLE "accounts_customuser_groups" (
"id" BIGSERIAL,
"customuser_id" BIGINT NOT NULL,
"group_id" INTEGER NOT NULL
);
-- Table: accounts_customuser_user_permissions
DROP TABLE IF EXISTS "accounts_customuser_user_permissions" CASCADE;
CREATE TABLE "accounts_customuser_user_permissions" (
"id" BIGSERIAL,
"customuser_id" BIGINT NOT NULL,
"permission_id" INTEGER NOT NULL
);
-- Table: auth_group
DROP TABLE IF EXISTS "auth_group" CASCADE;
CREATE TABLE "auth_group" (
"id" SERIAL,
"name" VARCHAR(150) NOT NULL
);
-- Table: auth_group_permissions
DROP TABLE IF EXISTS "auth_group_permissions" CASCADE;
CREATE TABLE "auth_group_permissions" (
"id" BIGSERIAL,
"group_id" INTEGER NOT NULL,
"permission_id" INTEGER NOT NULL
);
-- Table: auth_permission
DROP TABLE IF EXISTS "auth_permission" CASCADE;
CREATE TABLE "auth_permission" (
"id" SERIAL,
"name" VARCHAR(255) NOT NULL,
"content_type_id" INTEGER NOT NULL,
"codename" VARCHAR(100) NOT NULL
);
-- Data for table: auth_permission
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (1, 'Can add log entry', 1, 'add_logentry');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (2, 'Can change log entry', 1, 'change_logentry');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (3, 'Can delete log entry', 1, 'delete_logentry');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (4, 'Can view log entry', 1, 'view_logentry');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (5, 'Can add permission', 3, 'add_permission');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (6, 'Can change permission', 3, 'change_permission');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (7, 'Can delete permission', 3, 'delete_permission');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (8, 'Can view permission', 3, 'view_permission');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (9, 'Can add group', 2, 'add_group');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (10, 'Can change group', 2, 'change_group');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (11, 'Can delete group', 2, 'delete_group');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (12, 'Can view group', 2, 'view_group');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (13, 'Can add content type', 4, 'add_contenttype');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (14, 'Can change content type', 4, 'change_contenttype');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (15, 'Can delete content type', 4, 'delete_contenttype');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (16, 'Can view content type', 4, 'view_contenttype');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (17, 'Can add session', 5, 'add_session');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (18, 'Can change session', 5, 'change_session');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (19, 'Can delete session', 5, 'delete_session');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (20, 'Can view session', 5, 'view_session');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (21, 'Can add task result', 8, 'add_taskresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (22, 'Can change task result', 8, 'change_taskresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (23, 'Can delete task result', 8, 'delete_taskresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (24, 'Can view task result', 8, 'view_taskresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (25, 'Can add chord counter', 6, 'add_chordcounter');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (26, 'Can change chord counter', 6, 'change_chordcounter');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (27, 'Can delete chord counter', 6, 'delete_chordcounter');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (28, 'Can view chord counter', 6, 'view_chordcounter');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (29, 'Can add group result', 7, 'add_groupresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (30, 'Can change group result', 7, 'change_groupresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (31, 'Can delete group result', 7, 'delete_groupresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (32, 'Can view group result', 7, 'view_groupresult');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (33, 'Can add crontab', 10, 'add_crontabschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (34, 'Can change crontab', 10, 'change_crontabschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (35, 'Can delete crontab', 10, 'delete_crontabschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (36, 'Can view crontab', 10, 'view_crontabschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (37, 'Can add interval', 11, 'add_intervalschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (38, 'Can change interval', 11, 'change_intervalschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (39, 'Can delete interval', 11, 'delete_intervalschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (40, 'Can view interval', 11, 'view_intervalschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (41, 'Can add periodic task', 12, 'add_periodictask');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (42, 'Can change periodic task', 12, 'change_periodictask');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (43, 'Can delete periodic task', 12, 'delete_periodictask');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (44, 'Can view periodic task', 12, 'view_periodictask');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (45, 'Can add periodic tasks', 13, 'add_periodictasks');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (46, 'Can change periodic tasks', 13, 'change_periodictasks');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (47, 'Can delete periodic tasks', 13, 'delete_periodictasks');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (48, 'Can view periodic tasks', 13, 'view_periodictasks');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (49, 'Can add solar event', 14, 'add_solarschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (50, 'Can change solar event', 14, 'change_solarschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (51, 'Can delete solar event', 14, 'delete_solarschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (52, 'Can view solar event', 14, 'view_solarschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (53, 'Can add clocked', 9, 'add_clockedschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (54, 'Can change clocked', 9, 'change_clockedschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (55, 'Can delete clocked', 9, 'delete_clockedschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (56, 'Can view clocked', 9, 'view_clockedschedule');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (57, 'Can add association', 15, 'add_association');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (58, 'Can change association', 15, 'change_association');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (59, 'Can delete association', 15, 'delete_association');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (60, 'Can view association', 15, 'view_association');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (61, 'Can add code', 16, 'add_code');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (62, 'Can change code', 16, 'change_code');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (63, 'Can delete code', 16, 'delete_code');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (64, 'Can view code', 16, 'view_code');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (65, 'Can add nonce', 17, 'add_nonce');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (66, 'Can change nonce', 17, 'change_nonce');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (67, 'Can delete nonce', 17, 'delete_nonce');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (68, 'Can view nonce', 17, 'view_nonce');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (69, 'Can add user social auth', 19, 'add_usersocialauth');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (70, 'Can change user social auth', 19, 'change_usersocialauth');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (71, 'Can delete user social auth', 19, 'delete_usersocialauth');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (72, 'Can view user social auth', 19, 'view_usersocialauth');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (73, 'Can add partial', 18, 'add_partial');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (74, 'Can change partial', 18, 'change_partial');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (75, 'Can delete partial', 18, 'delete_partial');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (76, 'Can view partial', 18, 'view_partial');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (77, 'Can add site', 20, 'add_site');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (78, 'Can change site', 20, 'change_site');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (79, 'Can delete site', 20, 'delete_site');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (80, 'Can view site', 20, 'view_site');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (81, 'Can add user', 21, 'add_customuser');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (82, 'Can change user', 21, 'change_customuser');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (83, 'Can delete user', 21, 'delete_customuser');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (84, 'Can view user', 21, 'view_customuser');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (85, 'Can add Banner', 22, 'add_banner');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (86, 'Can change Banner', 22, 'change_banner');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (87, 'Can delete Banner', 22, 'delete_banner');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (88, 'Can view Banner', 22, 'view_banner');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (89, 'Can add Site Ayarı', 23, 'add_setting');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (90, 'Can change Site Ayarı', 23, 'change_setting');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (91, 'Can delete Site Ayarı', 23, 'delete_setting');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (92, 'Can view Site Ayarı', 23, 'view_setting');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (93, 'Can add Site Ayarı Aç / Kapat', 24, 'add_sitesettings');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (94, 'Can change Site Ayarı Aç / Kapat', 24, 'change_sitesettings');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (95, 'Can delete Site Ayarı Aç / Kapat', 24, 'delete_sitesettings');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (96, 'Can view Site Ayarı Aç / Kapat', 24, 'view_sitesettings');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (97, 'Can add Veritabanı Yedeği', 25, 'add_databasebackup');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (98, 'Can change Veritabanı Yedeği', 25, 'change_databasebackup');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (99, 'Can delete Veritabanı Yedeği', 25, 'delete_databasebackup');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (100, 'Can view Veritabanı Yedeği', 25, 'view_databasebackup');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (101, 'Can add Contact', 26, 'add_contact');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (102, 'Can change Contact', 26, 'change_contact');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (103, 'Can delete Contact', 26, 'delete_contact');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (104, 'Can view Contact', 26, 'view_contact');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (105, 'Can add Galeri Resmi', 29, 'add_images');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (106, 'Can change Galeri Resmi', 29, 'change_images');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (107, 'Can delete Galeri Resmi', 29, 'delete_images');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (108, 'Can view Galeri Resmi', 29, 'view_images');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (109, 'Can add Ürün Kategori', 27, 'add_category');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (110, 'Can change Ürün Kategori', 27, 'change_category');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (111, 'Can delete Ürün Kategori', 27, 'delete_category');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (112, 'Can view Ürün Kategori', 27, 'view_category');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (113, 'Can add Ürün Tagı', 31, 'add_tags');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (114, 'Can change Ürün Tagı', 31, 'change_tags');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (115, 'Can delete Ürün Tagı', 31, 'delete_tags');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (116, 'Can view Ürün Tagı', 31, 'view_tags');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (117, 'Can add Ürün', 30, 'add_product');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (118, 'Can change Ürün', 30, 'change_product');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (119, 'Can delete Ürün', 30, 'delete_product');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (120, 'Can view Ürün', 30, 'view_product');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (121, 'Can add Ürüm Yorum', 28, 'add_comment');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (122, 'Can change Ürüm Yorum', 28, 'change_comment');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (123, 'Can delete Ürüm Yorum', 28, 'delete_comment');
INSERT INTO "auth_permission" ("id", "name", "content_type_id", "codename") VALUES (124, 'Can view Ürüm Yorum', 28, 'view_comment');
-- Table: backup_databasebackup
DROP TABLE IF EXISTS "backup_databasebackup" CASCADE;
CREATE TABLE "backup_databasebackup" (
"id" BIGSERIAL,
"name" VARCHAR(255) NOT NULL,
"file_path" VARCHAR(500),
"file_size" BIGINT,
"status" VARCHAR(20) NOT NULL,
"backup_type" VARCHAR(20) NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"completed_at" TIMESTAMP WITH TIME ZONE,
"error_message" TEXT,
"notes" TEXT,
"created_by_id" BIGINT
);
-- Data for table: backup_databasebackup
INSERT INTO "backup_databasebackup" ("id", "name", "file_path", "file_size", "status", "backup_type", "created_at", "completed_at", "error_message", "notes", "created_by_id") VALUES (2, 'Manuel Yedek - 2026-01-18 22:29:47', '/Users/beyhan/Desktop/Projeler/Python/shop/backups/backup_shop_20260119_012947.sql', 53831, 'completed', 'manual', '2026-01-18T22:29:47.223287+00:00'::timestamptz, '2026-01-18T22:29:50.305187+00:00'::timestamptz, NULL, NULL, 1);
INSERT INTO "backup_databasebackup" ("id", "name", "file_path", "file_size", "status", "backup_type", "created_at", "completed_at", "error_message", "notes", "created_by_id") VALUES (3, 'Manuel Yedek - 2026-01-18 22:33:37', NULL, NULL, 'in_progress', 'manual', '2026-01-18T22:33:37.380995+00:00'::timestamptz, NULL, NULL, NULL, 1);
-- Table: banners
DROP TABLE IF EXISTS "banners" CASCADE;
CREATE TABLE "banners" (
"id" BIGSERIAL,
"color" VARCHAR(25) NOT NULL,
"title" VARCHAR(254),
"text1" VARCHAR(254),
"text2" VARCHAR(254),
"text4" VARCHAR(254),
"text5" VARCHAR(254),
"image" VARCHAR(100) NOT NULL,
"image_k" VARCHAR(100),
"image_k_txt" VARCHAR(254),
"is_active" BOOLEAN NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: categories
DROP TABLE IF EXISTS "categories" CASCADE;
CREATE TABLE "categories" (
"id" BIGSERIAL,
"title" VARCHAR(254) NOT NULL,
"keywords" VARCHAR(254) NOT NULL,
"description" VARCHAR(254) NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"is_active" BOOLEAN NOT NULL,
"order" INTEGER NOT NULL,
"slug" VARCHAR(250) NOT NULL,
"parent_id" BIGINT,
"images" VARCHAR(100)
);
-- Table: comments
DROP TABLE IF EXISTS "comments" CASCADE;
CREATE TABLE "comments" (
"id" BIGSERIAL,
"title" VARCHAR(254) NOT NULL,
"body" TEXT NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"is_active" BOOLEAN NOT NULL,
"slug" VARCHAR(50) NOT NULL,
"parent_id" BIGINT,
"user_id" BIGINT NOT NULL,
"product_id" BIGINT NOT NULL
);
-- Table: contacts
DROP TABLE IF EXISTS "contacts" CASCADE;
CREATE TABLE "contacts" (
"id" BIGSERIAL,
"name" VARCHAR(254) NOT NULL,
"email" VARCHAR(254) NOT NULL,
"subject" VARCHAR(254) NOT NULL,
"message" TEXT NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"user_id" BIGINT,
"ip" VARCHAR(100)
);
-- Table: django_admin_log
DROP TABLE IF EXISTS "django_admin_log" CASCADE;
CREATE TABLE "django_admin_log" (
"id" SERIAL,
"action_time" TIMESTAMP WITH TIME ZONE NOT NULL,
"object_id" TEXT,
"object_repr" VARCHAR(200) NOT NULL,
"action_flag" SMALLINT NOT NULL,
"change_message" TEXT NOT NULL,
"content_type_id" INTEGER,
"user_id" BIGINT NOT NULL
);
-- Data for table: django_admin_log
INSERT INTO "django_admin_log" ("id", "action_time", "object_id", "object_repr", "action_flag", "change_message", "content_type_id", "user_id") VALUES (1, '2026-01-18T22:29:34.026717+00:00'::timestamptz, '1', 'aaa - Bekliyor', 1, '[{"added": {}}]', 25, 1);
INSERT INTO "django_admin_log" ("id", "action_time", "object_id", "object_repr", "action_flag", "change_message", "content_type_id", "user_id") VALUES (2, '2026-01-18T22:32:58.428647+00:00'::timestamptz, '1', 'aaa - Bekliyor', 3, '', 25, 1);
-- Table: django_celery_beat_clockedschedule
DROP TABLE IF EXISTS "django_celery_beat_clockedschedule" CASCADE;
CREATE TABLE "django_celery_beat_clockedschedule" (
"id" BIGSERIAL,
"clocked_time" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: django_celery_beat_crontabschedule
DROP TABLE IF EXISTS "django_celery_beat_crontabschedule" CASCADE;
CREATE TABLE "django_celery_beat_crontabschedule" (
"id" BIGSERIAL,
"minute" VARCHAR(240) NOT NULL,
"hour" VARCHAR(96) NOT NULL,
"day_of_week" VARCHAR(64) NOT NULL,
"day_of_month" VARCHAR(124) NOT NULL,
"month_of_year" VARCHAR(64) NOT NULL,
"timezone" VARCHAR(63) NOT NULL
);
-- Table: django_celery_beat_intervalschedule
DROP TABLE IF EXISTS "django_celery_beat_intervalschedule" CASCADE;
CREATE TABLE "django_celery_beat_intervalschedule" (
"id" BIGSERIAL,
"every" INTEGER NOT NULL,
"period" VARCHAR(24) NOT NULL
);
-- Table: django_celery_beat_periodictask
DROP TABLE IF EXISTS "django_celery_beat_periodictask" CASCADE;
CREATE TABLE "django_celery_beat_periodictask" (
"id" BIGSERIAL,
"name" VARCHAR(200) NOT NULL,
"task" VARCHAR(200) NOT NULL,
"args" TEXT NOT NULL,
"kwargs" TEXT NOT NULL,
"queue" VARCHAR(200),
"exchange" VARCHAR(200),
"routing_key" VARCHAR(200),
"expires" TIMESTAMP WITH TIME ZONE,
"enabled" BOOLEAN NOT NULL,
"last_run_at" TIMESTAMP WITH TIME ZONE,
"total_run_count" INTEGER NOT NULL,
"date_changed" TIMESTAMP WITH TIME ZONE NOT NULL,
"description" TEXT NOT NULL,
"crontab_id" BIGINT,
"interval_id" BIGINT,
"solar_id" BIGINT,
"one_off" BOOLEAN NOT NULL,
"start_time" TIMESTAMP WITH TIME ZONE,
"priority" INTEGER,
"headers" TEXT NOT NULL,
"clocked_id" BIGINT,
"expire_seconds" INTEGER
);
-- Table: django_celery_beat_periodictasks
DROP TABLE IF EXISTS "django_celery_beat_periodictasks" CASCADE;
CREATE TABLE "django_celery_beat_periodictasks" (
"ident" SMALLINT NOT NULL,
"last_update" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: django_celery_beat_solarschedule
DROP TABLE IF EXISTS "django_celery_beat_solarschedule" CASCADE;
CREATE TABLE "django_celery_beat_solarschedule" (
"id" BIGSERIAL,
"event" VARCHAR(24) NOT NULL,
"latitude" NUMERIC NOT NULL,
"longitude" NUMERIC NOT NULL
);
-- Table: django_celery_results_chordcounter
DROP TABLE IF EXISTS "django_celery_results_chordcounter" CASCADE;
CREATE TABLE "django_celery_results_chordcounter" (
"id" SERIAL,
"group_id" VARCHAR(255) NOT NULL,
"sub_tasks" TEXT NOT NULL,
"count" INTEGER NOT NULL
);
-- Table: django_celery_results_groupresult
DROP TABLE IF EXISTS "django_celery_results_groupresult" CASCADE;
CREATE TABLE "django_celery_results_groupresult" (
"id" SERIAL,
"group_id" VARCHAR(255) NOT NULL,
"date_created" TIMESTAMP WITH TIME ZONE NOT NULL,
"date_done" TIMESTAMP WITH TIME ZONE NOT NULL,
"content_type" VARCHAR(128) NOT NULL,
"content_encoding" VARCHAR(64) NOT NULL,
"result" TEXT
);
-- Table: django_celery_results_taskresult
DROP TABLE IF EXISTS "django_celery_results_taskresult" CASCADE;
CREATE TABLE "django_celery_results_taskresult" (
"id" SERIAL,
"task_id" VARCHAR(255) NOT NULL,
"status" VARCHAR(50) NOT NULL,
"content_type" VARCHAR(128) NOT NULL,
"content_encoding" VARCHAR(64) NOT NULL,
"result" TEXT,
"date_done" TIMESTAMP WITH TIME ZONE NOT NULL,
"traceback" TEXT,
"meta" TEXT,
"task_args" TEXT,
"task_kwargs" TEXT,
"task_name" VARCHAR(255),
"worker" VARCHAR(100),
"date_created" TIMESTAMP WITH TIME ZONE NOT NULL,
"periodic_task_name" VARCHAR(255),
"date_started" TIMESTAMP WITH TIME ZONE
);
-- Table: django_content_type
DROP TABLE IF EXISTS "django_content_type" CASCADE;
CREATE TABLE "django_content_type" (
"id" SERIAL,
"app_label" VARCHAR(100) NOT NULL,
"model" VARCHAR(100) NOT NULL
);
-- Data for table: django_content_type
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (1, 'admin', 'logentry');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (2, 'auth', 'group');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (3, 'auth', 'permission');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (4, 'contenttypes', 'contenttype');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (5, 'sessions', 'session');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (6, 'django_celery_results', 'chordcounter');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (7, 'django_celery_results', 'groupresult');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (8, 'django_celery_results', 'taskresult');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (9, 'django_celery_beat', 'clockedschedule');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (10, 'django_celery_beat', 'crontabschedule');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (11, 'django_celery_beat', 'intervalschedule');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (12, 'django_celery_beat', 'periodictask');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (13, 'django_celery_beat', 'periodictasks');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (14, 'django_celery_beat', 'solarschedule');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (15, 'social_django', 'association');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (16, 'social_django', 'code');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (17, 'social_django', 'nonce');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (18, 'social_django', 'partial');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (19, 'social_django', 'usersocialauth');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (20, 'sites', 'site');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (21, 'accounts', 'customuser');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (22, 'settings', 'banner');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (23, 'settings', 'setting');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (24, 'settings', 'sitesettings');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (25, 'backup', 'databasebackup');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (26, 'contact', 'contact');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (27, 'product', 'category');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (28, 'product', 'comment');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (29, 'product', 'images');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (30, 'product', 'product');
INSERT INTO "django_content_type" ("id", "app_label", "model") VALUES (31, 'product', 'tags');
-- Table: django_migrations
DROP TABLE IF EXISTS "django_migrations" CASCADE;
CREATE TABLE "django_migrations" (
"id" BIGSERIAL,
"app" VARCHAR(255) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"applied" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Data for table: django_migrations
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (1, 'contenttypes', '0001_initial', '2026-01-18T22:26:26.197648+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (2, 'contenttypes', '0002_remove_content_type_name', '2026-01-18T22:26:26.360052+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (3, 'auth', '0001_initial', '2026-01-18T22:26:26.904480+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (4, 'auth', '0002_alter_permission_name_max_length', '2026-01-18T22:26:27.004323+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (5, 'auth', '0003_alter_user_email_max_length', '2026-01-18T22:26:27.071923+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (6, 'auth', '0004_alter_user_username_opts', '2026-01-18T22:26:27.167428+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (7, 'auth', '0005_alter_user_last_login_null', '2026-01-18T22:26:27.303966+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (8, 'auth', '0006_require_contenttypes_0002', '2026-01-18T22:26:27.391310+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (9, 'auth', '0007_alter_validators_add_error_messages', '2026-01-18T22:26:27.484909+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (10, 'auth', '0008_alter_user_username_max_length', '2026-01-18T22:26:27.577593+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (11, 'auth', '0009_alter_user_last_name_max_length', '2026-01-18T22:26:27.774582+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (12, 'auth', '0010_alter_group_name_max_length', '2026-01-18T22:26:27.931659+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (13, 'auth', '0011_update_proxy_permissions', '2026-01-18T22:26:27.999110+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (14, 'auth', '0012_alter_user_first_name_max_length', '2026-01-18T22:26:28.091754+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (15, 'accounts', '0001_initial', '2026-01-18T22:26:28.718278+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (16, 'admin', '0001_initial', '2026-01-18T22:26:28.997267+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (17, 'admin', '0002_logentry_remove_auto_add', '2026-01-18T22:26:29.035111+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (18, 'admin', '0003_logentry_add_action_flag_choices', '2026-01-18T22:26:29.132077+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (19, 'backup', '0001_initial', '2026-01-18T22:26:29.396877+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (20, 'contact', '0001_initial', '2026-01-18T22:26:29.498738+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (21, 'contact', '0002_contact_user', '2026-01-18T22:26:29.726625+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (22, 'contact', '0003_alter_contact_message', '2026-01-18T22:26:29.828197+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (23, 'contact', '0004_contact_ip', '2026-01-18T22:26:29.992327+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (24, 'contact', '0005_alter_contact_ip_alter_contact_user', '2026-01-18T22:26:30.323272+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (25, 'django_celery_beat', '0001_initial', '2026-01-18T22:26:30.768960+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (26, 'django_celery_beat', '0002_auto_20161118_0346', '2026-01-18T22:26:30.969010+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (27, 'django_celery_beat', '0003_auto_20161209_0049', '2026-01-18T22:26:31.072305+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (28, 'django_celery_beat', '0004_auto_20170221_0000', '2026-01-18T22:26:31.133956+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (29, 'django_celery_beat', '0005_add_solarschedule_events_choices', '2026-01-18T22:26:31.224248+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (30, 'django_celery_beat', '0006_auto_20180322_0932', '2026-01-18T22:26:31.519108+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (31, 'django_celery_beat', '0007_auto_20180521_0826', '2026-01-18T22:26:31.718953+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (32, 'django_celery_beat', '0008_auto_20180914_1922', '2026-01-18T22:26:31.798080+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (33, 'django_celery_beat', '0006_auto_20180210_1226', '2026-01-18T22:26:31.898867+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (34, 'django_celery_beat', '0006_periodictask_priority', '2026-01-18T22:26:32.054824+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (35, 'django_celery_beat', '0009_periodictask_headers', '2026-01-18T22:26:32.212981+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (36, 'django_celery_beat', '0010_auto_20190429_0326', '2026-01-18T22:26:32.426848+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (37, 'django_celery_beat', '0011_auto_20190508_0153', '2026-01-18T22:26:32.685772+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (38, 'django_celery_beat', '0012_periodictask_expire_seconds', '2026-01-18T22:26:32.783946+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (39, 'django_celery_beat', '0013_auto_20200609_0727', '2026-01-18T22:26:32.849509+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (40, 'django_celery_beat', '0014_remove_clockedschedule_enabled', '2026-01-18T22:26:33.003032+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (41, 'django_celery_beat', '0015_alter_clockedschedule_id_alter_crontabschedule_id_and_more', '2026-01-18T22:26:34.475445+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (42, 'django_celery_results', '0001_initial', '2026-01-18T22:26:34.772179+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (43, 'django_celery_results', '0002_add_task_name_args_kwargs', '2026-01-18T22:26:34.929787+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (44, 'django_celery_results', '0003_auto_20181106_1101', '2026-01-18T22:26:34.992086+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (45, 'django_celery_results', '0004_auto_20190516_0412', '2026-01-18T22:26:35.300946+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (46, 'django_celery_results', '0005_taskresult_worker', '2026-01-18T22:26:35.512788+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (47, 'django_celery_results', '0006_taskresult_date_created', '2026-01-18T22:26:35.748405+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (48, 'django_celery_results', '0007_remove_taskresult_hidden', '2026-01-18T22:26:35.842453+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (49, 'django_celery_results', '0008_chordcounter', '2026-01-18T22:26:36.040012+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (50, 'django_celery_results', '0009_groupresult', '2026-01-18T22:26:37.044478+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (51, 'django_celery_results', '0010_remove_duplicate_indices', '2026-01-18T22:26:37.197820+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (52, 'django_celery_results', '0011_taskresult_periodic_task_name', '2026-01-18T22:26:37.324394+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (53, 'django_celery_results', '0012_taskresult_date_started', '2026-01-18T22:26:37.447071+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (54, 'django_celery_results', '0013_taskresult_django_cele_periodi_1993cf_idx', '2026-01-18T22:26:37.567406+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (55, 'django_celery_results', '0014_alter_taskresult_status', '2026-01-18T22:26:37.630094+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (56, 'product', '0001_initial', '2026-01-18T22:26:39.186225+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (57, 'product', '0002_product_kd_price_alter_product_price', '2026-01-18T22:26:39.293780+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (58, 'product', '0003_rename_kd_price_product_kg_price', '2026-01-18T22:26:39.423004+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (59, 'product', '0004_alter_images_options_alter_images_images_and_more', '2026-01-18T22:26:39.681525+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (60, 'product', '0005_product_special_product_special_images', '2026-01-18T22:26:39.882839+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (61, 'product', '0006_product_thumbnail', '2026-01-18T22:26:40.010784+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (62, 'product', '0007_category_images', '2026-01-18T22:26:40.166425+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (63, 'product', '0008_remove_product_special_images', '2026-01-18T22:26:40.292882+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (64, 'product', '0009_remove_product_special', '2026-01-18T22:26:40.419665+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (65, 'product', '0010_alter_product_price', '2026-01-18T22:26:40.488409+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (66, 'product', '0011_remove_product_kg_price_product_brim', '2026-01-18T22:26:40.715387+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (67, 'product', '0012_alter_product_brim', '2026-01-18T22:26:40.783285+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (68, 'product', '0013_alter_product_brim', '2026-01-18T22:26:40.879267+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (69, 'product', '0014_alter_product_slug', '2026-01-18T22:26:40.975299+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (70, 'product', '0015_alter_product_brim', '2026-01-18T22:26:41.102201+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (71, 'product', '0016_product_gallery', '2026-01-18T22:26:41.516596+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (72, 'product', '0017_alter_product_brim', '2026-01-18T22:26:41.556929+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (73, 'product', '0018_product_is_front', '2026-01-18T22:26:41.750191+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (74, 'product', '0019_product_thumb', '2026-01-18T22:26:41.910670+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (75, 'product', '0020_remove_product_thumbnail_alter_product_thumb', '2026-01-18T22:26:42.051322+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (76, 'product', '0021_remove_product_thumb_alter_category_images_and_more', '2026-01-18T22:26:42.322768+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (77, 'sessions', '0001_initial', '2026-01-18T22:26:42.539586+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (78, 'settings', '0001_initial', '2026-01-18T22:26:42.693943+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (79, 'settings', '0002_setting_linkedin_setting_pinterest', '2026-01-18T22:26:42.914953+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (80, 'settings', '0003_sitesettings', '2026-01-18T22:26:43.040738+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (81, 'settings', '0004_alter_sitesettings_options_sitesettings_site_active', '2026-01-18T22:26:43.197700+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (82, 'settings', '0005_alter_sitesettings_site_active', '2026-01-18T22:26:43.259407+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (83, 'sites', '0001_initial', '2026-01-18T22:26:43.415004+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (84, 'sites', '0002_alter_domain_unique', '2026-01-18T22:26:43.566011+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (85, 'default', '0001_initial', '2026-01-18T22:26:44.103081+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (86, 'social_auth', '0001_initial', '2026-01-18T22:26:44.133067+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (87, 'social_django', '0001_initial', '2026-01-18T22:26:44.162641+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (88, 'default', '0002_add_related_name', '2026-01-18T22:26:44.211596+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (89, 'social_auth', '0002_add_related_name', '2026-01-18T22:26:44.268276+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (90, 'social_django', '0002_add_related_name', '2026-01-18T22:26:44.297551+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (91, 'default', '0003_alter_email_max_length', '2026-01-18T22:26:44.421917+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (92, 'social_auth', '0003_alter_email_max_length', '2026-01-18T22:26:44.451711+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (93, 'social_django', '0003_alter_email_max_length', '2026-01-18T22:26:44.481836+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (94, 'default', '0004_auto_20160423_0400', '2026-01-18T22:26:44.553779+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (95, 'social_auth', '0004_auto_20160423_0400', '2026-01-18T22:26:44.613238+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (96, 'social_django', '0004_auto_20160423_0400', '2026-01-18T22:26:44.643021+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (97, 'social_auth', '0005_auto_20160727_2333', '2026-01-18T22:26:44.765049+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (98, 'social_django', '0005_auto_20160727_2333', '2026-01-18T22:26:44.795770+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (99, 'social_django', '0006_partial', '2026-01-18T22:26:45.017004+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (100, 'social_django', '0007_code_timestamp', '2026-01-18T22:26:45.203222+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (101, 'social_django', '0008_partial_timestamp', '2026-01-18T22:26:45.390796+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (102, 'social_django', '0009_auto_20191118_0520', '2026-01-18T22:26:45.606051+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (103, 'social_django', '0010_uid_db_index', '2026-01-18T22:26:45.768091+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (104, 'social_django', '0011_alter_id_fields', '2026-01-18T22:26:46.483286+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (105, 'social_django', '0012_usersocialauth_extra_data_new', '2026-01-18T22:26:46.719674+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (106, 'social_django', '0013_migrate_extra_data', '2026-01-18T22:26:46.965822+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (107, 'social_django', '0014_remove_usersocialauth_extra_data', '2026-01-18T22:26:47.134938+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (108, 'social_django', '0015_rename_extra_data_new_usersocialauth_extra_data', '2026-01-18T22:26:47.297024+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (109, 'social_django', '0016_alter_usersocialauth_extra_data', '2026-01-18T22:26:47.369579+00:00'::timestamptz);
INSERT INTO "django_migrations" ("id", "app", "name", "applied") VALUES (110, 'social_django', '0017_usersocialauth_user_social_auth_uid_required', '2026-01-18T22:26:47.531898+00:00'::timestamptz);
-- Table: django_session
DROP TABLE IF EXISTS "django_session" CASCADE;
CREATE TABLE "django_session" (
"session_key" VARCHAR(40) NOT NULL,
"session_data" TEXT NOT NULL,
"expire_date" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Data for table: django_session
INSERT INTO "django_session" ("session_key", "session_data", "expire_date") VALUES ('tcqskuvq8ylqqsnym8li5ryz5msuh07l', '.eJxVjDsOwjAQRO_iGln-x6GkzxmstXeNA8iR4qRC3B1HSgHdaN6bebMA-1bC3mgNM7Irk-zy20VIT6oHwAfU-8LTUrd1jvxQ-Ekbnxak1-10_w4KtNLXlLMln1COkI0atHMWRp81AYERpIX1yigXM9IAHnvWVngw0nSuyLPPF_siOAc:1vhbFg:OLstq7hX4uJnLrchGxLbzykgqQqPzsfUd738yJCTISA', '2026-02-01T22:28:08.313123+00:00'::timestamptz);
-- Table: django_site
DROP TABLE IF EXISTS "django_site" CASCADE;
CREATE TABLE "django_site" (
"id" SERIAL,
"domain" VARCHAR(100) NOT NULL,
"name" VARCHAR(50) NOT NULL
);
-- Data for table: django_site
INSERT INTO "django_site" ("id", "domain", "name") VALUES (1, 'example.com', 'example.com');
-- Table: images
DROP TABLE IF EXISTS "images" CASCADE;
CREATE TABLE "images" (
"id" BIGSERIAL,
"title" VARCHAR(254) NOT NULL,
"is_active" BOOLEAN NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"images" VARCHAR(100)
);
-- Table: products
DROP TABLE IF EXISTS "products" CASCADE;
CREATE TABLE "products" (
"id" BIGSERIAL,
"title" VARCHAR(254) NOT NULL,
"content" TEXT,
"keywords" VARCHAR(254) NOT NULL,
"price" DOUBLE PRECISION NOT NULL,
"video" VARCHAR(254),
"slug" VARCHAR(250) NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"is_active" BOOLEAN NOT NULL,
"images" VARCHAR(100),
"brim" VARCHAR(10) NOT NULL,
"is_front" BOOLEAN NOT NULL
);
-- Table: products_categories
DROP TABLE IF EXISTS "products_categories" CASCADE;
CREATE TABLE "products_categories" (
"id" BIGSERIAL,
"product_id" BIGINT NOT NULL,
"category_id" BIGINT NOT NULL
);
-- Table: products_gallery
DROP TABLE IF EXISTS "products_gallery" CASCADE;
CREATE TABLE "products_gallery" (
"id" BIGSERIAL,
"product_id" BIGINT NOT NULL,
"images_id" BIGINT NOT NULL
);
-- Table: products_tags
DROP TABLE IF EXISTS "products_tags" CASCADE;
CREATE TABLE "products_tags" (
"id" BIGSERIAL,
"product_id" BIGINT NOT NULL,
"tags_id" BIGINT NOT NULL
);
-- Table: settings
DROP TABLE IF EXISTS "settings" CASCADE;
CREATE TABLE "settings" (
"id" BIGSERIAL,
"title" VARCHAR(254) NOT NULL,
"meta_title" VARCHAR(254) NOT NULL,
"meta_description" VARCHAR(254) NOT NULL,
"phone" VARCHAR(254) NOT NULL,
"url" VARCHAR(254),
"email" VARCHAR(254) NOT NULL,
"facebook" VARCHAR(254),
"x" VARCHAR(254),
"instagram" VARCHAR(254),
"whatsapp" VARCHAR(254),
"slogan" VARCHAR(254),
"w_logo" VARCHAR(100),
"b_logo" VARCHAR(100),
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"is_active" BOOLEAN NOT NULL,
"linkedin" VARCHAR(254),
"pinterest" VARCHAR(254)
);
-- Table: site_settings
DROP TABLE IF EXISTS "site_settings" CASCADE;
CREATE TABLE "site_settings" (
"id" BIGSERIAL,
"is_active" BOOLEAN NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"site_active" BOOLEAN NOT NULL
);
-- Table: social_auth_association
DROP TABLE IF EXISTS "social_auth_association" CASCADE;
CREATE TABLE "social_auth_association" (
"id" BIGSERIAL,
"server_url" VARCHAR(255) NOT NULL,
"handle" VARCHAR(255) NOT NULL,
"secret" VARCHAR(255) NOT NULL,
"issued" INTEGER NOT NULL,
"lifetime" INTEGER NOT NULL,
"assoc_type" VARCHAR(64) NOT NULL
);
-- Table: social_auth_code
DROP TABLE IF EXISTS "social_auth_code" CASCADE;
CREATE TABLE "social_auth_code" (
"id" BIGSERIAL,
"email" VARCHAR(254) NOT NULL,
"code" VARCHAR(32) NOT NULL,
"verified" BOOLEAN NOT NULL,
"timestamp" TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Table: social_auth_nonce
DROP TABLE IF EXISTS "social_auth_nonce" CASCADE;
CREATE TABLE "social_auth_nonce" (
"id" BIGSERIAL,
"server_url" VARCHAR(255) NOT NULL,
"timestamp" INTEGER NOT NULL,
"salt" VARCHAR(65) NOT NULL
);
-- Table: social_auth_partial
DROP TABLE IF EXISTS "social_auth_partial" CASCADE;
CREATE TABLE "social_auth_partial" (
"id" BIGSERIAL,
"token" VARCHAR(32) NOT NULL,
"next_step" SMALLINT NOT NULL,
"backend" VARCHAR(32) NOT NULL,
"timestamp" TIMESTAMP WITH TIME ZONE NOT NULL,
"data" JSONB NOT NULL
);
-- Table: social_auth_usersocialauth
DROP TABLE IF EXISTS "social_auth_usersocialauth" CASCADE;
CREATE TABLE "social_auth_usersocialauth" (
"id" BIGSERIAL,
"provider" VARCHAR(32) NOT NULL,
"uid" VARCHAR(255) NOT NULL,
"user_id" BIGINT NOT NULL,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"modified" TIMESTAMP WITH TIME ZONE NOT NULL,
"extra_data" JSONB NOT NULL
);
-- Table: tags
DROP TABLE IF EXISTS "tags" CASCADE;
CREATE TABLE "tags" (
"id" BIGSERIAL,
"tag" VARCHAR(254) NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"is_active" BOOLEAN NOT NULL,
"slug" VARCHAR(50) NOT NULL
);
-- Reset sequences
SELECT setval('django_migrations_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_migrations"));
SELECT setval('django_content_type_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_content_type"));
SELECT setval('auth_permission_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "auth_permission"));
SELECT setval('auth_group_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "auth_group"));
SELECT setval('auth_group_permissions_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "auth_group_permissions"));
SELECT setval('accounts_customuser_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "accounts_customuser"));
SELECT setval('accounts_customuser_groups_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "accounts_customuser_groups"));
SELECT setval('accounts_customuser_user_permissions_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "accounts_customuser_user_permissions"));
SELECT setval('django_admin_log_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_admin_log"));
SELECT setval('backup_databasebackup_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "backup_databasebackup"));
SELECT setval('contacts_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "contacts"));
SELECT setval('django_celery_beat_crontabschedule_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_beat_crontabschedule"));
SELECT setval('django_celery_beat_intervalschedule_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_beat_intervalschedule"));
SELECT setval('django_celery_beat_periodictask_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_beat_periodictask"));
SELECT setval('django_celery_beat_solarschedule_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_beat_solarschedule"));
SELECT setval('django_celery_beat_clockedschedule_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_beat_clockedschedule"));
SELECT setval('django_celery_results_taskresult_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_results_taskresult"));
SELECT setval('django_celery_results_chordcounter_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_results_chordcounter"));
SELECT setval('django_celery_results_groupresult_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_celery_results_groupresult"));
SELECT setval('images_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "images"));
SELECT setval('categories_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "categories"));
SELECT setval('tags_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "tags"));
SELECT setval('products_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "products"));
SELECT setval('products_categories_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "products_categories"));
SELECT setval('products_tags_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "products_tags"));
SELECT setval('comments_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "comments"));
SELECT setval('products_gallery_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "products_gallery"));
SELECT setval('banners_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "banners"));
SELECT setval('settings_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "settings"));
SELECT setval('site_settings_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "site_settings"));
SELECT setval('django_site_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "django_site"));
SELECT setval('social_auth_association_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "social_auth_association"));
SELECT setval('social_auth_code_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "social_auth_code"));
SELECT setval('social_auth_nonce_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "social_auth_nonce"));
SELECT setval('social_auth_usersocialauth_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "social_auth_usersocialauth"));
SELECT setval('social_auth_partial_id_seq', (SELECT COALESCE(MAX("id"), 1) FROM "social_auth_partial"));

0
blog/__init__.py Normal file
View File

197
blog/admin.py Normal file
View File

@@ -0,0 +1,197 @@
from django.contrib import admin
from django.utils.html import format_html
from blog.models import PCategory, Post, PComment, PCategoryView
class ChildInline(admin.TabularInline):
"""Alt kategorileri inline olarak göster"""
model = PCategory
extra = 0
fk_name = 'parent'
fields = ('title', 'is_active', 'order', 'slug')
readonly_fields = ('slug',)
@admin.register(PCategory)
class PCategoryAdmin(admin.ModelAdmin):
list_display = ('title', 'parent', 'is_active', 'order', 'created_at', 'post_count', 'view_count', 'image_preview')
list_filter = ('is_active', 'created_at', 'parent')
search_fields = ('title', 'keywords', 'description', 'slug')
list_editable = ('order', 'is_active')
prepopulated_fields = {'slug': ('title',)}
readonly_fields = ('created_at', 'updated_at', 'image_preview', 'view_count')
fieldsets = (
('Temel Bilgiler', {
'fields': ('title', 'parent', 'order', 'is_active')
}),
('SEO Bilgileri', {
'fields': ('slug', 'keywords', 'description')
}),
('Görsel', {
'fields': ('image', 'image_preview')
}),
('Tarihler', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
inlines = [ChildInline]
def image_preview(self, obj):
if obj.image:
return format_html('<img src="{}" width="100" height="100" style="object-fit: cover;" />', obj.image.url)
return "Resim Yok"
image_preview.short_description = 'Resim Önizleme'
def post_count(self, obj):
return obj.post_categories.count()
post_count.short_description = 'Post Sayısı'
def view_count(self, obj):
return obj.category_views.count()
view_count.short_description = 'Görüntülenme'
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.prefetch_related('post_categories', 'category_views')
return queryset
class PostCategoriesInline(admin.TabularInline):
"""Post kategorilerini inline olarak göster"""
model = Post.categories.through
extra = 1
verbose_name = 'Kategori'
verbose_name_plural = 'Kategoriler'
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'user', 'is_active', 'is_front', 'created_at', 'comment_count', 'image_preview')
list_filter = ('is_active', 'is_front', 'created_at', 'updated_at', 'categories')
search_fields = ('title', 'content', 'keywords', 'slug')
list_editable = ('is_active', 'is_front')
readonly_fields = ('created_at', 'updated_at', 'slug', 'image_preview', 'thumb_preview', 'thumb', 'parent')
filter_horizontal = ('categories',)
fieldsets = (
('Temel Bilgiler', {
'fields': ('title', 'user', 'content', 'categories')
}),
('SEO ve Medya', {
'fields': ('slug', 'keywords', 'video')
}),
('Görseller', {
'fields': ('image', 'image_preview', 'thumb_preview'),
'description': 'Thumb otomatik oluşturulur, image yüklediğinizde.'
}),
('Durum', {
'fields': ('is_active', 'is_front', 'parent')
}),
('Tarihler', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def image_preview(self, obj):
if obj.image:
return format_html('<img src="{0}" width="150" height="90" style="object-fit: cover; border: 1px solid #ddd;" />', obj.image.url)
return "Resim Yok"
image_preview.short_description = 'Ana Resim Önizleme'
def thumb_preview(self, obj):
if obj.thumb:
return format_html('<img src="{0}" width="100" height="60" style="object-fit: cover; border: 1px solid #ddd;" />', obj.thumb.url)
return "Thumb Yok (Kaydet ve otomatik oluşur)"
thumb_preview.short_description = 'Thumb Önizleme'
def comment_count(self, obj):
return obj._post.filter(is_active=True).count()
comment_count.short_description = 'Yorum Sayısı'
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.prefetch_related('categories', '_post').select_related('user')
return queryset
def save_model(self, request, obj, form, change):
"""Post kaydedilirken user otomatik atanabilir"""
if not obj.user:
obj.user = request.user
super().save_model(request, obj, form, change)
class ChildCommentInline(admin.TabularInline):
"""Alt yorumları inline olarak göster"""
model = PComment
extra = 0
fk_name = 'parent'
fields = ('user', 'title', 'body', 'is_active')
readonly_fields = ('user',)
@admin.register(PComment)
class PCommentAdmin(admin.ModelAdmin):
list_display = ('title', 'user', 'post', 'parent', 'is_active', 'created_at', 'child_count')
list_filter = ('is_active', 'created_at', 'post')
search_fields = ('title', 'body', 'user__username', 'user__email', 'post__title')
list_editable = ('is_active',)
readonly_fields = ('created_at', 'updated_at', 'slug', 'user', 'post')
fieldsets = (
('Yorum Bilgileri', {
'fields': ('user', 'post', 'parent')
}),
('İçerik', {
'fields': ('title', 'body', 'slug')
}),
('Durum', {
'fields': ('is_active',)
}),
('Tarihler', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
inlines = [ChildCommentInline]
def child_count(self, obj):
return obj.child.count()
child_count.short_description = 'Alt Yorum Sayısı'
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.select_related('user', 'post', 'parent').prefetch_related('child')
return queryset
@admin.register(PCategoryView)
class PCategoryViewAdmin(admin.ModelAdmin):
list_display = ('category', 'ip_address', 'created_at', 'short_user_agent')
list_filter = ('created_at', 'category')
search_fields = ('ip_address', 'user_agent', 'category__title')
readonly_fields = ('category', 'ip_address', 'user_agent', 'created_at')
date_hierarchy = 'created_at'
def short_user_agent(self, obj):
if obj.user_agent:
return obj.user_agent[:50] + '...' if len(obj.user_agent) > 50 else obj.user_agent
return '-'
short_user_agent.short_description = 'User Agent'
def has_add_permission(self, request):
"""Kategori ziyaretleri manuel eklenemez"""
return False
def has_change_permission(self, request, obj=None):
"""Kategori ziyaretleri düzenlenemez"""
return False
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.select_related('category')
return queryset

9
blog/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'
default_auto_field = 'django.db.models.BigAutoField'
def ready(self):
import blog.signals

View File

View File

View File

@@ -0,0 +1,85 @@
import os
import random
import string
import requests
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.core.files.base import ContentFile
from blog.models import Post, Category, Tags
try:
from faker import Faker
fake = Faker()
HAS_FAKER = True
except ImportError:
HAS_FAKER = False
class Command(BaseCommand):
help = 'Creates 300 fake posts with random images'
def handle(self, *args, **kwargs):
User = get_user_model()
# Prefer an existing staff user, otherwise any existing user, otherwise create a fallback user
user = User.objects.filter(is_staff=True).first() or User.objects.first()
if not user:
self.stdout.write('Hiç kullanıcı bulunamadı, `fakeuser` oluşturuluyor...')
user = User.objects.create(username='fakeuser', email='fake@example.com')
user.set_unusable_password()
user.save()
categories = list(Category.objects.all())
if not categories:
self.stdout.write(self.style.ERROR('Lütfen önce en az bir kategori oluşturun!'))
return
tags = list(Tags.objects.all())
if not tags:
self.stdout.write('Tag bulunamadı, oluşturuluyor...')
for i in range(5):
tag_name = self.get_random_string(8) if not HAS_FAKER else fake.word()
Tags.objects.create(tag=tag_name)
tags = list(Tags.objects.all())
self.stdout.write('300 adet fake post oluşturuluyor...')
for i in range(300):
if HAS_FAKER:
title = fake.sentence(nb_words=6).replace('.', '')
content = '\n\n'.join(fake.paragraphs(nb=5))
keywords = ", ".join(fake.words(nb=5))
else:
title = self.get_random_string(30)
content = self.get_random_string(500)
keywords = self.get_random_string(20)
post = Post(
user=user,
title=title,
content=content,
keywords=keywords,
video='none',
is_active=True,
is_front=True
)
post.save()
# ManyToMany ilişkileri
post.categories.add(random.choice(categories))
post.tags.add(random.choice(tags))
# Resim ekle
try:
# Picsum'dan rastgele resim (800x600)
img_url = f"https://picsum.photos/seed/{random.randint(1, 10000)}/800/600"
response = requests.get(img_url, timeout=10)
if response.status_code == 200:
file_name = f"fake_post_{i}_{random.randint(1000,9999)}.jpg"
post.image.save(file_name, ContentFile(response.content), save=True)
self.stdout.write(f'Post {i+1}/300 oluşturuldu: {title} (Resimli)')
else:
self.stdout.write(f'Post {i+1}/300 oluşturuldu: {title} (Resimsiz - İndirme hatası)')
except Exception as e:
self.stdout.write(f'Post {i+1}/300 oluşturuldu: {title} (Resimsiz - Hata: {str(e)})')
def get_random_string(self, length):
letters = string.ascii_letters + string.digits + ' '
return ''.join(random.choice(letters) for i in range(length))

View File

@@ -0,0 +1,38 @@
from django.core.management.base import BaseCommand
from blog.models import Post
class Command(BaseCommand):
help = 'Tüm blog postları için eksik thumb dosyalarını oluşturur'
def handle(self, *args, **options):
posts = Post.objects.filter(image__isnull=False)
total = posts.count()
created = 0
skipped = 0
self.stdout.write(f'\n{total} post kontrol ediliyor...\n')
for post in posts:
if not post.thumb:
try:
post.save() # save() metodu thumb'ı otomatik oluşturacak
created += 1
self.stdout.write(
self.style.SUCCESS(f'✓ Thumb oluşturuldu: {post.title}')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'✗ Hata ({post.title}): {str(e)}')
)
else:
skipped += 1
self.stdout.write(
self.style.WARNING(f'- Atlandı (zaten var): {post.title}')
)
self.stdout.write(
self.style.SUCCESS(
f'\n✓ Tamamlandı! {created} thumb oluşturuldu, {skipped} atlandı.\n'
)
)

View File

@@ -0,0 +1,109 @@
# Generated by Django 6.0 on 2026-01-21 00:59
import autoslug.fields
import core.utils
import django.db.models.deletion
import imagekit.models.fields
import tinymce.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='PCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=254, verbose_name='Kategori')),
('keywords', models.CharField(max_length=254, verbose_name='Seo Kelimeleri Aralarına Virgül Koyunuz')),
('description', models.CharField(max_length=254, verbose_name='ıklama')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı')),
('order', models.IntegerField(db_index=True, default=1, verbose_name='Görüntülenme Sırası')),
('slug', autoslug.fields.AutoSlugField(blank=True, editable=True, max_length=250, populate_from='title', unique=True)),
('image', imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/category'), verbose_name='Resim 630 x 653 Olmali ve Transparan PNG Olmali')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='blog.pcategory', verbose_name='Üst Kategorisi')),
],
options={
'verbose_name': 'Post Kategori',
'verbose_name_plural': 'Post Kategorilerileri',
'db_table': 'p_categories',
'ordering': ['order'],
'unique_together': {('slug', 'parent')},
},
),
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=254, verbose_name='Post Başlığı')),
('content', tinymce.models.HTMLField(blank=True, null=True, verbose_name='Post İçeriği')),
('keywords', models.CharField(max_length=254, verbose_name='Seo Kelimeleri Aralarına Virgül Koyunuz')),
('image', imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/post'))),
('thumb', imagekit.models.fields.ProcessedImageField(blank=True, editable=False, null=True, upload_to=core.utils.UniquePathAndRename('uploads/post/thumb'))),
('video', models.CharField(blank=True, default='none', max_length=254, null=True, verbose_name='Video')),
('slug', autoslug.fields.AutoSlugField(blank=True, editable=True, max_length=250, populate_from='title', unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı ?')),
('is_front', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Önde Görünsünmü ?')),
('categories', models.ManyToManyField(related_name='post_categories', to='blog.pcategory', verbose_name='Post Kategorisi')),
('parent', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='blog.post', verbose_name='Konular')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Post',
'verbose_name_plural': 'Posts',
'db_table': 'posts',
'ordering': ['created_at'],
'unique_together': {('slug',)},
},
),
migrations.CreateModel(
name='PCategoryView',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip_address', models.GenericIPAddressField(verbose_name='IP Adresi')),
('user_agent', models.TextField(blank=True, null=True, verbose_name='User Agent')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Ziyaret Tarihi')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_views', to='blog.pcategory')),
],
options={
'verbose_name': 'Kategori Ziyareti',
'verbose_name_plural': 'Kategori Ziyaretleri',
'db_table': 'p_category_views',
'indexes': [models.Index(fields=['category', 'ip_address', 'created_at'], name='p_category__categor_8efae6_idx')],
},
),
migrations.CreateModel(
name='PComment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=254, verbose_name='Yorum Başlığı')),
('body', models.TextField(verbose_name='Yorum')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='blog.pcomment')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_cuser', to=settings.AUTH_USER_MODEL)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_post', to='blog.post')),
],
options={
'verbose_name': 'Post Yorum',
'verbose_name_plural': 'Post Yorumları',
'db_table': 'p_comments',
'ordering': ['-created_at'],
'unique_together': {('slug', 'parent')},
},
),
]

View File

216
blog/models.py Normal file
View File

@@ -0,0 +1,216 @@
import os
from autoslug import AutoSlugField
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models
from imagekit.models import ProcessedImageField
from tinymce.models import HTMLField
from core.utils import image_optimizer
# Create your models here.
class PCategory(models.Model):
aktif = (
(True, 'Evet'),
(False, 'Hayır'),
)
title = models.CharField(max_length=254, verbose_name="Kategori")
keywords = models.CharField(max_length=254, verbose_name="Seo Kelimeleri Aralarına Virgül Koyunuz")
description = models.CharField(max_length=254, verbose_name="ıklama")
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
is_active = models.BooleanField(default=True, verbose_name='Yayındamı', choices=aktif)
order = models.IntegerField(verbose_name='Görüntülenme Sırası', default=1, db_index=True)
slug = AutoSlugField(populate_from='title', null=False, unique=True, editable=True, db_index=True, max_length=250,
blank=True)
parent = models.ForeignKey('self', related_name='child', on_delete=models.CASCADE, blank=True, null=True,
verbose_name='Üst Kategorisi')
image = ProcessedImageField(**image_optimizer('uploads/category', 300, 300, 85, 'PNG'),
verbose_name='Resim 630 x 653 Olmali ve Transparan PNG Olmali', blank=True, null=True)
class Meta:
ordering = ["order"]
db_table = 'p_categories'
verbose_name_plural = "Post Kategorilerileri"
verbose_name = "Post Kategori"
unique_together = ('slug', 'parent',)
def get_slug(self):
slug = self.title.replace('ı', "i").replace('İ', 'i')
number = 1
while PCategory.objects.filter(slug=slug).exists():
slug = '{}-{}'.format(slug, number)
number += 1
return slug
def save(self, *args, **kwargs):
if not self.slug:
self.slug = self.get_slug()
super().save(*args, **kwargs)
def __str__(self):
full_path = [self.title]
k = self.parent
while k is not None:
full_path.append(k.title)
k = k.parent
return ' -> '.join(full_path[::-1])
class Post(models.Model):
aktif = (
(True, 'Evet'),
(False, 'Hayır'),
)
title = models.CharField(max_length=254, verbose_name="Post Başlığı")
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='posts', null=True, blank=True)
content = HTMLField(blank=True, null=True, verbose_name='Post İçeriği')
categories = models.ManyToManyField(PCategory, verbose_name="Post Kategorisi", related_name='post_categories')
keywords = models.CharField(max_length=254, verbose_name="Seo Kelimeleri Aralarına Virgül Koyunuz")
image = ProcessedImageField(**image_optimizer('uploads/post', 840, 500, 85, 'avif'), null=True, blank=True)
thumb = ProcessedImageField(**image_optimizer('uploads/post/thumb', 348, 160, 85, 'avif'), null=True, blank=True,editable=False)
video = models.CharField(verbose_name="Video", null=True, blank=True, max_length=254, default='none')
slug = AutoSlugField(populate_from='title', null=False, unique=True, editable=True, db_index=True, max_length=250,
blank=True)
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
is_active = models.BooleanField(default=True, verbose_name='Yayındamı ?', choices=aktif)
is_front = models.BooleanField(default=True, verbose_name='Önde Görünsünmü ?', choices=aktif)
parent = models.ForeignKey('self', related_name='child', on_delete=models.CASCADE, blank=True, null=True,
editable=False,
verbose_name='Konular')
class Meta:
ordering = ["created_at"]
db_table = 'posts'
verbose_name_plural = "Posts"
verbose_name = "Post"
unique_together = ('slug',)
def get_slug(self):
slug = self.title.replace('ı', "i").replace('İ', 'i')
number = 1
while Post.objects.filter(slug=slug).exists():
slug = '{}-{}'.format(slug, number)
number += 1
return slug
def save(self, *args, **kwargs):
if not self.slug:
self.slug = self.get_slug()
if self.image:
# Eğer yeni bir kayıt ise veya resim değişmişse veya thumb yoksa
update_thumb = False
if not self.pk:
update_thumb = True
elif not self.thumb:
# Thumb yoksa oluştur
update_thumb = True
else:
try:
old_instance = self.__class__.objects.get(pk=self.pk)
if self.image != old_instance.image:
update_thumb = True
except self.__class__.DoesNotExist:
pass
if update_thumb:
try:
if hasattr(self.image, 'closed') and self.image.closed:
self.image.open()
if hasattr(self.image, 'seek'):
self.image.seek(0)
content = self.image.read()
filename = os.path.basename(self.image.name)
# Doğrudan alana ata, super().save() işleyecek
self.thumb = ContentFile(content, name=filename)
except Exception:
pass
finally:
if hasattr(self.image, 'seek'):
self.image.seek(0)
super().save(*args, **kwargs)
def __str__(self):
return f"Postlar: {self.title}"
class PCategoryView(models.Model):
"""Kategori ziyaretlerini takip etmek için model"""
category = models.ForeignKey(PCategory, on_delete=models.CASCADE, related_name='category_views')
ip_address = models.GenericIPAddressField(verbose_name='IP Adresi')
user_agent = models.TextField(blank=True, null=True, verbose_name='User Agent')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Ziyaret Tarihi')
class Meta:
db_table = 'p_category_views'
verbose_name = 'Kategori Ziyareti'
verbose_name_plural = 'Kategori Ziyaretleri'
# unique_together kısıtlamasını kaldırdık - artık günlük bazda kontrol edeceğiz
indexes = [
models.Index(fields=['category', 'ip_address', 'created_at']),
]
def __str__(self):
return f"{self.category.title} - {self.ip_address} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"
class PComment(models.Model):
aktif = (
(True, 'Evet'),
(False, 'Hayır'),
)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='_cuser')
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='_post')
title = models.CharField(max_length=254, verbose_name="Yorum Başlığı")
body = models.TextField(verbose_name='Yorum')
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
is_active = models.BooleanField(default=True, verbose_name='Yayındamı', choices=aktif)
slug = AutoSlugField(populate_from='title', null=False, unique=True, editable=False, db_index=True)
parent = models.ForeignKey('self', related_name='child', on_delete=models.CASCADE, blank=True, null=True)
class Meta:
ordering = ["-created_at"]
db_table = 'p_comments'
verbose_name_plural = "Post Yorumları"
verbose_name = "Post Yorum"
unique_together = ('slug', 'parent',)
def get_slug(self):
slug = self.title.replace('ı', "i").replace('İ', 'i')
number = 1
while Comment.objects.filter(slug=slug).exists():
slug = '{}-{}'.format(slug, number)
number += 1
return slug
def save(self, *args, **kwargs):
if not self.slug:
self.slug = self.get_slug()
if self.parent:
self.post = self.parent.post
if self.parent.parent:
self.parent = self.parent.parent
super().save(*args, **kwargs)
def __str__(self):
full_path = [self.title]
k = self.parent
while k is not None:
full_path.append(k.title)
k = k.parent
return ' -> '.join(full_path[::-1])

153
blog/serializers.py Normal file
View File

@@ -0,0 +1,153 @@
from rest_framework import serializers
from blog.models import PCategory, Post, PComment
from blog.tasks import send_comment_notification_email
class CateSerializer(serializers.ModelSerializer):
parent = serializers.StringRelatedField() # ID yerine __str__ metodundaki değeri döndürür
class Meta:
model = PCategory
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'image', 'keywords', 'description']
class CommentSerializer(serializers.ModelSerializer):
child = serializers.SerializerMethodField()
user = serializers.StringRelatedField(read_only=True)
class Meta:
model = PComment
fields = ['id', 'user', 'post', 'title', 'body', 'created_at', 'slug', 'parent', 'child']
read_only_fields = ['slug', 'created_at', 'user']
def get_child(self, obj):
# Sadece aktif alt yorumları getir
children = obj.child.filter(is_active=True).order_by('created_at')
return CommentSerializer(children, many=True).data
def create(self, validated_data):
# Kullanıcıyı request'ten al
request = self.context.get('request')
if request and hasattr(request, 'user'):
validated_data['user'] = request.user
instance = super().create(validated_data)
# Celery task'ini tetikle
# Kullanıcı email'i varsa al, yoksa username kullan
user_email = instance.user.email if instance.user.email else instance.user.username
send_comment_notification_email.delay(
comment_title=instance.title,
comment_body=instance.body,
post_title=instance.post.title,
user_email=user_email
)
return instance
class PostSerializer(serializers.ModelSerializer):
categories = CateSerializer(read_only=True, many=True)
comments = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
thumb = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id','title', 'content', 'categories', 'keywords', 'image', 'thumb', 'video',
'slug', 'created_at', 'updated_at', 'is_active', 'is_front', 'comments']
# fields = '__all__'
def get_image(self, obj):
if obj.image:
# Sadece path kısmını döndür (media/ ile başlayan kısım)
url = obj.image.url
# URL'de domain varsa çıkar, yoksa olduğu gibi döndür
if 'http://' in url or 'https://' in url:
# URL'den sadece path kısmını al
from urllib.parse import urlparse
parsed = urlparse(url)
return parsed.path
return url
return None
def get_thumb(self, obj):
if obj.thumb:
# Sadece path kısmını döndür (media/ ile başlayan kısım)
url = obj.thumb.url
# URL'de domain varsa çıkar, yoksa olduğu gibi döndür
if 'http://' in url or 'https://' in url:
# URL'den sadece path kısmını al
from urllib.parse import urlparse
parsed = urlparse(url)
return parsed.path
return url
return None
def get_comments(self, obj):
# Sadece ana yorumları (parent=None) ve aktif olanları getir
comments = obj._post.filter(parent__isnull=True, is_active=True).order_by('-created_at')
return CommentSerializer(comments, many=True).data
class PostSYalinerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['slug', ]
# fields = '__all__'
class CategorySerializer(serializers.ModelSerializer):
posts = PostSYalinerializer(source='c_categories', read_only=True, many=True)
child = serializers.SerializerMethodField()
class Meta:
model = PCategory
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'image', 'keywords', 'description',
'posts', 'child']
def get_child(self, obj):
serializer = self.__class__(obj.child.all(), many=True, context=self.context)
return serializer.data
class CategoryPostSerializer(serializers.ModelSerializer):
posts = serializers.SerializerMethodField()
child = serializers.SerializerMethodField()
class Meta:
model = PCategory
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'image', 'keywords', 'description',
'posts', 'child']
def get_posts(self, obj):
# Pagination context'ini al
paginator = self.context.get('paginator')
request = self.context.get('request')
posts = obj.c_categories.all()
if paginator and request:
# Pagination uygula
paginated_posts = paginator.paginate_queryset(posts, request)
serializer = PostSerializer(paginated_posts, many=True, context=self.context)
return {
'results': serializer.data,
'count': posts.count(),
'next': paginator.get_next_link(),
'previous': paginator.get_previous_link(),
}
else:
# Pagination yoksa normal döndür
serializer = PostSerializer(posts, many=True, context=self.context)
return serializer.data
def get_child(self, obj):
serializer = self.__class__(obj.child.all(), many=True, context=self.context)
return serializer.data

52
blog/signals.py Normal file
View File

@@ -0,0 +1,52 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.core.files.base import ContentFile
from .models import Post
@receiver(pre_save, sender=Post)
def update_post_thumb(sender, instance, **kwargs):
"""
Post kaydedilmeden önce, image alanı doluysa thumb'ı da güncelle
"""
if instance.image:
# Yeni kayıt veya image güncellenmiş mi kontrol et
should_update_thumb = False
if instance.pk:
try:
old_instance = Post.objects.get(pk=instance.pk)
# Image değişmişse thumb'ı da güncelle
if str(old_instance.image) != str(instance.image):
should_update_thumb = True
except Post.DoesNotExist:
# Kayıt bulunamadı, yeni kayıt gibi davran
should_update_thumb = True
else:
# Yeni kayıt (pk yok)
should_update_thumb = True
if should_update_thumb and hasattr(instance.image, 'file'):
# Image dosyasını thumb alanına kopyala
try:
# Image dosyasının içeriğini oku
instance.image.file.seek(0)
image_content = instance.image.file.read()
instance.image.file.seek(0)
# Dosya adını al
image_name = instance.image.name.split('/')[-1]
# Thumb alanına kaydet
instance.thumb.save(
image_name,
ContentFile(image_content),
save=False
)
except Exception as e:
print(f"Thumb oluşturma hatası: {e}")
import traceback
traceback.print_exc()

40
blog/tasks.py Normal file
View File

@@ -0,0 +1,40 @@
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings
@shared_task
def send_comment_notification_email(comment_title, comment_body, post_title, user_email):
"""
Yeni bir yorum yapıldığında admin'e e-posta gönderir.
"""
subject = f'Yeni Yorum: {post_title}'
message = f"""
Merhaba Admin,
"{post_title}" başlıklı yazıya yeni bir yorum yapıldı.
Yorum Yapan: {user_email}
Başlık: {comment_title}
Yorum: {comment_body}
Kontrol etmek için admin paneline giriş yapabilirsiniz.
"""
# Admin e-posta adresini settings'den veya doğrudan buraya yazabilirsiniz
# Örnek olarak settings.DEFAULT_FROM_EMAIL kullanıldı, admin listesi de kullanılabilir
admin_email = settings.DEFAULT_FROM_EMAIL
# Eğer settings.ADMINS tanımlıysa oradaki ilk kişiye de atılabilir
if hasattr(settings, 'ADMINS') and settings.ADMINS:
recipient_list = [email for name, email in settings.ADMINS]
else:
# Fallback olarak bir email
recipient_list = ['admin@example.com']
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
recipient_list,
fail_silently=False,
)

3
blog/tests.py Normal file
View File

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

11
blog/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from blog.views import CategoryList, CategoryDetail, PostDetail, PostList, CommentCreate
urlpatterns = [
path('categories/', CategoryList.as_view(), name='categories.list'),
path('categories/<slug:slug>/', CategoryDetail.as_view(), name='categories.details'),
path('post/', PostList.as_view(), name='post.list'),
path('post/<slug:slug>/', PostDetail.as_view(), name='post.details'),
path('comment/create/', CommentCreate.as_view(), name='comment.create'),
]

53
blog/views.py Normal file
View File

@@ -0,0 +1,53 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView, CreateAPIView
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from blog.models import Post, PCategory, PComment
from blog.serializers import PostSerializer, CategorySerializer, CategoryPostSerializer, CommentSerializer
from core.Permission import ReadOnly
class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
# Create your views here.
class CategoryList(ListAPIView):
permission_classes = [ReadOnly]
queryset = PCategory.objects.order_by('order').filter(is_active=True, parent__isnull=True).all()
serializer_class = CategorySerializer
class CategoryDetail(RetrieveAPIView):
permission_classes = [ReadOnly]
queryset = PCategory.objects.order_by('order').filter(is_active=True).all()
serializer_class = CategoryPostSerializer
lookup_field = 'slug' # Slug ile arama yapılacak
def get_serializer_context(self):
context = super().get_serializer_context()
context['paginator'] = StandardResultsSetPagination()
return context
# Create your views here.
class PostList(ListAPIView):
permission_classes = [ReadOnly]
queryset = Post.objects.all()
serializer_class = PostSerializer
pagination_class = StandardResultsSetPagination
class PostDetail(RetrieveAPIView):
permission_classes = [ReadOnly]
queryset = Post.objects.all()
serializer_class = PostSerializer
lookup_field = 'slug' # Slug ile arama yapılacak
class CommentCreate(CreateAPIView):
queryset = PComment.objects.all()
serializer_class = CommentSerializer
permission_classes = [IsAuthenticated]

140
cart/API_DOCS.md Normal file
View File

@@ -0,0 +1,140 @@
# Sepet (Cart) API Kullanım Kılavuzu
Bu doküman, alışveriş sepeti (Shopping Cart) API'sinin nasıl kullanılacağını, uç noktaları (endpoints), istek parametrelerini ve örnek senaryoları içerir.
## Genel Bilgiler
- **Base URL:** `/api/v1/cart/`
- **Mantık:** Sepet, sunucu tarafında **Session (Oturum)** tabanlı çalışır.
- **Önemli Not:** İstemci (Frontend/Mobile), sunucudan dönen `sessionid` çerezini (cookie) saklamalı ve sonraki tüm isteklerde header içinde geri göndermelidir. Aksi takdirde her istekte yeni, boş bir sepet oluşturulur.
---
## Uç Noktalar (Endpoints)
### 1. Sepeti Görüntüle
Mevcut sepetin içeriğini ve toplam tutarını getirir.
- **URL:** `/api/v1/cart/`
- **Method:** `GET`
**Örnek Yanıt:**
```json
{
"items": [
{
"product": {
"id": 1,
"title": "Örnek Ürün",
"price": 100.00,
"images": "http://localhost:8000/media/...",
...
},
"quantity": 2,
"price": "100.00",
"total_price": "200.00"
}
],
"total_price": "200.00"
}
```
---
### 2. Sepete Ürün Ekle / Güncelle
Sepete yeni bir ürün ekler veya mevcut ürünün miktarını değiştirir.
- **URL:** `/api/v1/cart/add/`
- **Method:** `POST`
**Parametreler (Body - JSON):**
| Parametre | Tip | Zorunlu | Açıklama |
|-----------|-----|---------|----------|
| `product_id` | Integer | Evet | Eklenecek ürünün ID'si. |
| `quantity` | Integer | Hayır | Miktar (Varsayılan: 1). |
| `override_quantity` | Boolean | Hayır | `true` ise miktarı direkt eşitler, `false` ise mevcut miktarın üzerine ekler (Varsayılan: `false`). |
#### Senaryo A: Sepete Ürün Ekleme (veya Miktar Artırma)
Mevcut miktarın üzerine ekler. (Örn: Sepette 1 tane var, 2 tane daha ekle = 3 olur).
**İstek:**
```json
{
"product_id": 1,
"quantity": 2,
"override_quantity": false
}
```
#### Senaryo B: Miktarı Güncelleme / Azaltma
Miktarı direkt olarak belirtilen sayıya eşitler. (Örn: Sepette 5 tane var, 4'e düşürmek istiyorsunuz).
**İstek:**
```json
{
"product_id": 1,
"quantity": 4,
"override_quantity": true
}
```
#### Senaryo C: Miktarı Sıfırlayarak Silme
Eğer `override_quantity: true` iken `quantity: 0` gönderirseniz, ürün sepetten silinir.
**İstek:**
```json
{
"product_id": 1,
"quantity": 0,
"override_quantity": true
}
```
---
### 3. Sepetten Ürün Silme
Belirli bir ürünü sepetten tamamen kaldırır.
- **URL:** `/api/v1/cart/remove/<product_id>/`
- **Method:** `DELETE`
**Örnek:** `/api/v1/cart/remove/1/`
**Yanıt:** Güncel sepet içeriğini döndürür (Ekleme işlemiyle aynı formatta).
---
### 4. Sepeti Temizle
Sepetteki tüm ürünleri siler.
- **URL:** `/api/v1/cart/clear/`
- **Method:** `POST`
**Yanıt:**
```json
{
"message": "Cart cleared"
}
```
---
## Frontend Entegrasyonu İçin İpuçları (React/Vue/Mobile)
1. **Cookie Yönetimi:** Axios veya Fetch kullanırken `credentials: 'include'` veya `withCredentials: true` ayarının açık olduğundan emin olun. Bu, Django'nun session cookie'sini tarayıcının saklamasını ve göndermesini sağlar.
```javascript
// Axios Örneği
axios.post('/api/v1/cart/add/', data, {
withCredentials: true
});
```
2. **Ürün Detayları:** Sepet yanıtı (`items` dizisi) içindeki `product` objesi, `ProductSerializer`'dan gelen tüm veriyi (resim, slug, başlık vb.) içerir. Ekstra bir istek atmanıza gerek yoktur.
3. **Toplam Fiyat:** Sepet toplamı `total_price` alanında string decimal olarak gelir (Örn: "1250.50"). Frontend'de gösterirken formatlamanız gerekebilir.

0
cart/__init__.py Normal file
View File

3
cart/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

9
cart/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class CartConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cart'
def ready(self):
import cart.signals

127
cart/cart.py Normal file
View File

@@ -0,0 +1,127 @@
from decimal import Decimal
from django.conf import settings
from product.models import Product
from .models import Cart as CartModel, CartItem
class Cart(object):
def __init__(self, request):
"""
Initialize the cart.
"""
self.session = request.session
self.user = request.user
# Session cart initialization
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
def add(self, product, quantity=1, override_quantity=False):
"""
Add a product to the cart or update its quantity.
"""
if self.user.is_authenticated:
self._add_db(product, quantity, override_quantity)
else:
self._add_session(product, quantity, override_quantity)
def _add_session(self, product, quantity, override_quantity):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {'quantity': 0, 'price': str(product.price)}
if override_quantity:
self.cart[product_id]['quantity'] = quantity
else:
self.cart[product_id]['quantity'] += quantity
if self.cart[product_id]['quantity'] <= 0:
self.remove(product)
else:
self.save()
def _add_db(self, product, quantity, override_quantity):
cart, created = CartModel.objects.get_or_create(user=self.user)
cart_item, item_created = CartItem.objects.get_or_create(cart=cart, product=product)
if override_quantity:
cart_item.quantity = quantity
else:
if not item_created:
cart_item.quantity += quantity
else:
cart_item.quantity = quantity # Yeni oluşturulduysa zaten default 1 değil, gelen quantity olmalı
if cart_item.quantity <= 0:
cart_item.delete()
else:
cart_item.save()
def save(self):
# mark the session as "modified" to make sure it gets saved
self.session.modified = True
def remove(self, product):
"""
Remove a product from the cart.
"""
if self.user.is_authenticated:
CartItem.objects.filter(cart__user=self.user, product=product).delete()
else:
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
def __iter__(self):
"""
Iterate over the items in the cart and get the products
from the database.
"""
if self.user.is_authenticated:
# DB'den oku
cart, created = CartModel.objects.get_or_create(user=self.user)
for item in cart.items.select_related('product').all():
yield {
'product': item.product,
'quantity': item.quantity,
'price': Decimal(item.product.price),
'total_price': Decimal(item.product.price) * item.quantity
}
else:
# Session'dan oku
product_ids = self.cart.keys()
products = Product.objects.filter(id__in=product_ids)
cart = self.cart.copy()
for product in products:
cart[str(product.id)]['product'] = product
for item in cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = item['price'] * item['quantity']
yield item
def __len__(self):
"""
Count all items in the cart.
"""
if self.user.is_authenticated:
cart, created = CartModel.objects.get_or_create(user=self.user)
return sum(item.quantity for item in cart.items.all())
else:
return sum(item['quantity'] for item in self.cart.values())
def get_total_price(self):
if self.user.is_authenticated:
cart, created = CartModel.objects.get_or_create(user=self.user)
return sum(item.total_price for item in cart.items.all())
else:
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
def clear(self):
if self.user.is_authenticated:
cart, created = CartModel.objects.get_or_create(user=self.user)
cart.items.all().delete()
else:
del self.session[settings.CART_SESSION_ID]
self.save()

View File

@@ -0,0 +1,4 @@
from .cart import Cart
def cart(request):
return {'cart': Cart(request)}

11
cart/forms.py Normal file
View File

@@ -0,0 +1,11 @@
from django import forms
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]
class CartAddProductForm(forms.Form):
quantity = forms.TypedChoiceField(
choices=PRODUCT_QUANTITY_CHOICES,
coerce=int)
override = forms.BooleanField(required=False,
initial=False,
widget=forms.HiddenInput)

View File

@@ -0,0 +1,39 @@
# Generated by Django 6.0 on 2026-01-19 14:31
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('product', '0022_alter_product_content_alter_product_images'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Cart',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='cart', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CartItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='cart.cart')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product.product')),
],
options={
'unique_together': {('cart', 'product')},
},
),
]

View File

33
cart/models.py Normal file
View File

@@ -0,0 +1,33 @@
from django.db import models
from django.conf import settings
from product.models import Product
class Cart(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cart')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.email} - Cart"
def get_total_price(self):
return sum(item.total_price for item in self.items.all())
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
class Meta:
unique_together = ('cart', 'product')
def __str__(self):
return f"{self.cart.user.email} - {self.product.title}"
@property
def price(self):
return self.product.price
@property
def total_price(self):
return self.price * self.quantity

25
cart/serializers.py Normal file
View File

@@ -0,0 +1,25 @@
from rest_framework import serializers
from product.models import Product
from product.serializers import ProductSerializer
# CartItemSerializer artık hem dict (session) hem de model instance (db) ile çalışabilmeli.
# Serializer'lar varsayılan olarak object attribute'larına erişir.
# Dict erişimi için source kullanabiliriz veya to_representation override edebiliriz.
# Ancak en temizi, view tarafında veriyi standart bir yapıya (list of dicts) dönüştürüp serializer'a vermektir.
# Cart sınıfındaki __iter__ metodu zaten bunu yapıyor (hem DB hem Session için dict döndürüyor).
# Bu yüzden mevcut serializer yapısını koruyabiliriz.
class CartItemSerializer(serializers.Serializer):
product = ProductSerializer(read_only=True)
quantity = serializers.IntegerField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
total_price = serializers.DecimalField(max_digits=10, decimal_places=2)
class CartSerializer(serializers.Serializer):
items = CartItemSerializer(many=True, read_only=True)
total_price = serializers.DecimalField(max_digits=10, decimal_places=2)
class CartAddProductSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=0, default=1)
override_quantity = serializers.BooleanField(required=False, default=False)

39
cart/signals.py Normal file
View File

@@ -0,0 +1,39 @@
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from django.conf import settings
from .models import Cart, CartItem
from product.models import Product
@receiver(user_logged_in)
def merge_cart_on_login(sender, user, request, **kwargs):
"""
Kullanıcı giriş yaptığında session sepetini veritabanı sepetiyle birleştirir.
"""
session_cart = request.session.get(settings.CART_SESSION_ID)
if session_cart:
# Kullanıcının DB sepetini al veya oluştur
db_cart, created = Cart.objects.get_or_create(user=user)
for product_id, item_data in session_cart.items():
quantity = item_data['quantity']
product = Product.objects.get(id=product_id)
# Ürün zaten DB sepetinde var mı?
cart_item, item_created = CartItem.objects.get_or_create(cart=db_cart, product=product)
if not item_created:
# Varsa miktarı artır
cart_item.quantity += quantity
else:
# Yoksa miktarı ayarla (default 1 olduğu için üzerine eklemiyoruz, direkt atıyoruz ama get_or_create default ile oluşturduysa quantity 1 olabilir, o yüzden dikkat)
# get_or_create default=1 ile oluşturur. Biz session'dan geleni kullanmalıyız.
# Ancak item_created True ise yeni oluştu demektir ve default değeri almıştır.
# Bizim session'daki quantity'yi atamamız lazım.
cart_item.quantity = quantity
cart_item.save()
# Session sepetini temizle
del request.session[settings.CART_SESSION_ID]
request.session.modified = True

3
cart/tests.py Normal file
View File

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

11
cart/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = 'cart'
urlpatterns = [
path('', views.CartDetailView.as_view(), name='cart_detail'),
path('add/', views.CartAddView.as_view(), name='cart_add'),
path('remove/<int:product_id>/', views.CartRemoveView.as_view(), name='cart_remove'),
path('clear/', views.CartClearView.as_view(), name='cart_clear'),
]

66
cart/views.py Normal file
View File

@@ -0,0 +1,66 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
from product.models import Product
from .cart import Cart
from .serializers import CartSerializer, CartAddProductSerializer
class CartDetailView(APIView):
def get(self, request):
cart = Cart(request)
# Cart.__iter__ zaten dict döndürüyor, direkt listeye çevirebiliriz.
cart_items = list(cart)
data = {
'items': cart_items,
'total_price': cart.get_total_price()
}
serializer = CartSerializer(data)
return Response(serializer.data)
class CartAddView(APIView):
def post(self, request):
serializer = CartAddProductSerializer(data=request.data)
if serializer.is_valid():
product_id = serializer.validated_data['product_id']
quantity = serializer.validated_data['quantity']
override_quantity = serializer.validated_data['override_quantity']
product = get_object_or_404(Product, id=product_id)
cart = Cart(request)
cart.add(product=product, quantity=quantity, override_quantity=override_quantity)
# Güncel sepeti döndür
cart_items = list(cart)
data = {
'items': cart_items,
'total_price': cart.get_total_price()
}
return Response(CartSerializer(data).data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CartRemoveView(APIView):
def delete(self, request, product_id):
product = get_object_or_404(Product, id=product_id)
cart = Cart(request)
cart.remove(product)
# Güncel sepeti döndür
cart_items = list(cart)
data = {
'items': cart_items,
'total_price': cart.get_total_price()
}
return Response(CartSerializer(data).data, status=status.HTTP_200_OK)
class CartClearView(APIView):
def post(self, request):
cart = Cart(request)
cart.clear()
return Response({'message': 'Cart cleared'}, status=status.HTTP_200_OK)

View File

@@ -0,0 +1 @@
{"web":{"client_id":"915364976256-691m0s87as2r5vdbqr96f6humblseobt.apps.googleusercontent.com","project_id":"django-471018","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost:3000/api/auth/callback/google","http://localhost:8000/api/auth/callback/google","http://127.0.0.1:8000/api/auth/callback/google","http://127.0.0.1:3000/api/auth/callback/google","http://127.0.0.1:8000/accounts/auth/google/login/callback/","http://127.0.0.1:8000/auth/google/callback/","http://localhost:8000/auth/google/callback/","http://localhost:3000/auth/google","http://localhost:8000/auth/google"],"javascript_origins":["http://localhost:3000","http://localhost:8000","http://127.0.0.1:8000","http://127.0.0.1:3000"]}}

0
contact/__init__.py Normal file
View File

11
contact/admin.py Normal file
View File

@@ -0,0 +1,11 @@
from django.contrib import admin
from .models import Contact
# Register your models here.
class ContactAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'subject', 'created_at')
list_filter = ('created_at',)
search_fields = ('name', 'email', 'subject', 'message')
readonly_fields = ('created_at', 'updated_at')
admin.site.register(Contact, ContactAdmin)

5
contact/apps.py Normal file
View File

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

View File

@@ -0,0 +1,32 @@
# Generated by Django 6.0 on 2026-01-15 11:59
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Contact',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=254, verbose_name='Ad Soyad ')),
('email', models.CharField(max_length=254, verbose_name='Eposta Adresi ')),
('subject', models.CharField(max_length=254, verbose_name='Konu ')),
('message', models.CharField(max_length=254, verbose_name='Mesaj ')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
],
options={
'verbose_name': 'Contact',
'verbose_name_plural': 'Contact',
'db_table': 'contacts',
'ordering': ['created_at'],
},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 6.0 on 2026-01-15 12:39
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contact', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='contact',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Kullanıcı'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2026-01-15 12:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contact', '0002_contact_user'),
]
operations = [
migrations.AlterField(
model_name='contact',
name='message',
field=models.TextField(verbose_name='Mesaj '),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 6.0 on 2026-01-15 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contact', '0003_alter_contact_message'),
]
operations = [
migrations.AddField(
model_name='contact',
name='ip',
field=models.CharField(default=123, max_length=100, verbose_name='IP Adresi '),
preserve_default=False,
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 6.0 on 2026-01-15 13:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contact', '0004_contact_ip'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='contact',
name='ip',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='IP Adresi '),
),
migrations.AlterField(
model_name='contact',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Kullanıcı'),
),
]

View File

22
contact/models.py Normal file
View File

@@ -0,0 +1,22 @@
from django.db import models
# Create your models here.
class Contact(models.Model):
user = models.ForeignKey('accounts.CustomUser', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kullanıcı")
name = models.CharField(max_length=254, verbose_name="Ad Soyad ")
email = models.CharField(max_length=254, verbose_name="Eposta Adresi ")
ip = models.CharField(max_length=100, verbose_name="IP Adresi ", blank=True, null=True)
subject = models.CharField(max_length=254, verbose_name="Konu ")
message = models.TextField(verbose_name="Mesaj ")
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
class Meta:
ordering = ["created_at"]
db_table = 'contacts'
verbose_name_plural = "Contact"
verbose_name = "Contact"
def __str__(self):
return f"Contact: {self.name}"

10
contact/serializers.py Normal file
View File

@@ -0,0 +1,10 @@
from rest_framework import serializers
from contact.models import Contact
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ['id', 'name', 'email', 'subject', 'message', 'created_at']
read_only_fields = ['id', 'created_at']

40
contact/tasks.py Normal file
View File

@@ -0,0 +1,40 @@
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings
@shared_task
def send_contact_email(name, email, subject, message, ip=None):
"""
Contact formundan gelen mesajı email ile gönderir
"""
email_subject = f"Yeni İletişim Mesajı: {subject}"
email_message = f"""
Yeni bir iletişim mesajı alındı!
Gönderen: {name}
Email: {email}
IP Adresi: {ip or 'Belirtilmemiş'}
Konu: {subject}
Mesaj:
{message}
---
Bu mesaj otomatik olarak gönderilmiştir.
"""
try:
send_mail(
subject=email_subject,
message=email_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=['info@denizogur.com.tr'], # Mesajların gönderileceği email adresi
fail_silently=False,
)
return f"Email başarıyla gönderildi: {email}"
except Exception as e:
return f"Email gönderilemedi: {str(e)}"

3
contact/tests.py Normal file
View File

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

7
contact/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import path
from contact.views import ContactCreate
urlpatterns = [
path('contact/create/', ContactCreate.as_view(), name='contact.create'),
]

42
contact/views.py Normal file
View File

@@ -0,0 +1,42 @@
from rest_framework.generics import CreateAPIView
from rest_framework.permissions import AllowAny
from contact.models import Contact
from contact.serializers import ContactSerializer
from contact.tasks import send_contact_email
# Create your views here.
class ContactCreate(CreateAPIView):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
permission_classes = [AllowAny] # Herkes contact gönderebilir
def get_client_ip(self, request):
"""İstemcinin IP adresini al"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def perform_create(self, serializer):
# IP adresini al
ip_address = self.get_client_ip(self.request)
# Kullanıcı varsa kaydet, yoksa None olarak kaydet
user = self.request.user if self.request.user.is_authenticated else None
# Contact'ı kaydet
contact = serializer.save(user=user, ip=ip_address)
# Celery task ile email gönder (arka planda)
send_contact_email.delay(
name=contact.name,
email=contact.email,
subject=contact.subject,
message=contact.message,
ip=ip_address
)

View File

@@ -0,0 +1,314 @@
# Copilot için Ayrıntılı Prompt: Django 6.0 Custom Account + Djoser + JWT + Social Auth
Aşağıdaki metni **Copilot Chat** veya GitHub Copilota vereceğim. Sen (Copilot) bu projede adım adım ilerleyen bir **backend mimarı + uygulayıcı** gibi davranacaksın.
## Proje Bağlamı
- Framework: **Django==6.0**
- API: **Django REST Framework (djangorestframework==3.16.1)**
- Auth:
- **Djoser==2.3.3**
- **djangorestframework_simplejwt==5.5.1**
- Ek olarak **social auth** social-auth-app-django==5.6.0 social-auth-core==4.8.1 bunlar kulanilacak
- Bu proje bir **REST backend** olacak ve **Nuxt.js** ile **Next.js** frontendlere servis verecek.
- SPA (Single Page App) + JWT kullanımına uygun olacak.
- CORS, JSON response formatı, JWT bearer token desteği önemli.(Corsheader Paketi yuklu ve yaplandirlmis)
## İstediğim Auth Sistemi (Özet)
1. **Custom User Model** (email tabanlı):
- Login **email ile** yapılacak (username yok).
- Register da email + password ile olacak.
- `AUTH_USER_MODEL` olarak özel bir user modeli (örn. `accounts.CustomUser`) kullanılacak.
- `USERNAME_FIELD = "email"`
- `email` unique olacak.
- Normal (email/password) register ile oluşturulan hesaplar:
- Başlangıçta `is_active = False` olacak.
- Kullanıcı eposta aktivasyonu yapmadan giriş yapamayacak.
- **Social login** ile gelen hesaplar:
- `is_active = True` olacak (yani sosyal hesaplar için ayrıca email aktivasyon istemeyeceğiz).
2. **Register Akışı (Email/Password)**:
- Örnek endpoint (Djoser varsayılanı da olabilir): `POST /auth/register/`
- Request body:
```json
{
"email": "user@example.com",
"password": "StrongP@ssw0rd",
"re_password": "StrongP@ssw0rd",
"first_name": "Ali",
"last_name": "Veli"
}
```
- İşleyiş:
- Yeni user oluşturulur, `is_active = False` atanır.
- Djoser üzerinden aktivasyon epostası gönderilir.
- Aktivasyon linki Djoserin `ACTIVATION_URL` formatına göre hazırlanır:
- Örn: `https://frontend-domain/auth/activate/{uid}/{token}/` (Nuxt/Next içindeki sayfaya yönlenebilir).
3. **Email Aktivasyon**:
- Endpoint (Djoser): `POST /auth/activate/`
- Request body:
```json
{
"uid": "<uid_from_email>",
"token": "<token_from_email>"
}
```
- Aktivasyon başarılı olursa:
- `user.is_active = True`
- Kullanıcı artık JWT ile login olabilecek.
4. **Login (JWT ile)**:
- JWT auth için `djangorestframework_simplejwt` kullanılacak.
- Djoserla entegre endpoint:
- `POST /auth/jwt/create/`
- Request body:
```json
{
"email": "user@example.com",
"password": "StrongP@ssw0rd"
}
```
- Response:
```json
{
"access": "<jwt_access_token>",
"refresh": "<jwt_refresh_token>"
}
```
- **Aktivasyon yapmamış** kullanıcı login olmaya çalışırsa:
- Uygun bir hata kodu (400/401) ve anlamlı bir hata mesajı dönecek:
- Örn: `"detail": "Account is not activated. Please check your email."`
5. **Social Login**:
- Özel social auth endpointleri istiyorum:
- Örnek: `POST /auth/social/<provider>/` (ör: `/auth/social/google/`, `/auth/social/github/`).
- Frontend (Nuxt/Next) tarafı:
- Genelde OAuth flowu client-side yapıp backende `access_token` gönderiyor.
- Backend tarafı:
- Providerdan gelen `access_token` ile kullanıcı bilgisi alınacak.
- Providerdan email alınamazsa, uygun bir hata dönülecek (misal: `"Email not provided by provider"`).
- Eğer email varsa:
- Kullanıcı bulunursa giriş yaptır, yoksa yeni bir kullanıcı oluştur.
- Bu kullanıcı için:
- `is_active = True` olarak set edilecek.
- **Ekstra email aktivasyon epostası gönderilmeyecek**.
- Ardından JWT access/refresh token üret ve response olarak dön:
```json
{
"access": "<jwt_access_token>",
"refresh": "<jwt_refresh_token>",
"user": {
"id": 1,
"email": "user@example.com",
"first_name": "Ali",
"last_name": "Veli"
}
}
```
6. **Djoser Yapılandırması**:
- `djoser==2.3.3` kullanılacak.
- `settings.py` içinde `DJOSER = { ... }` konfigürasyonu yapılacak.
- Örnek ayarlar:
- `SEND_ACTIVATION_EMAIL = True`
- `ACTIVATION_URL = "auth/activate/{uid}/{token}/"` (buraya Nuxt/Next üzerindeki routeu da koyabiliriz).
- `SERIALIZERS` içinde:
- `user_create` → custom serializer (register sırasında `is_active=False` ayarlayacak).
- `user` → custom user serializer (frontende gönderilecek user alanlarını kontrol etmek için).
- Token modeli kullanmayacaksak `TOKEN_MODEL = None` vs.
7. **JWT Ayarları (SIMPLE_JWT)**:
- `REST_FRAMEWORK` → `DEFAULT_AUTHENTICATION_CLASSES` içinde `rest_framework_simplejwt.authentication.JWTAuthentication` tanımlanacak.
- `SIMPLE_JWT` içinde access/refresh lifetime, `AUTH_HEADER_TYPES = ("Bearer",)` vb. ayarları yapacağız.
8. **Email Gönderimi**:
- Geliştirme ortamı:
- `EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"` olabilir.
- Prod ortamı:
- ENV üzerinden SMTP ayarları (`EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD`, `EMAIL_USE_TLS` vb.)
- Aktivasyon için HTML ve plain text templateleri:
- `templates/email/activation_email.html`
- `templates/email/activation_email.txt`
- sindilik localdeki emailpit kullanalim
9. **CORS / SPA Entegrasyonu**:
- Nuxt ve Next için:
- `CORS_ALLOWED_ORIGINS` içinde dev ve prod URLler olacak (örn. `http://localhost:3000`, `http://localhost:5173`, vs.).
- Auth:
- JWT bearer token kullanılacak: `Authorization: Bearer <access_token>`
- Cookie tabanlı session auth kullanmayacağız (veya minimal). CSRF, SPA senaryosuna göre ayarlanacak / devre dışı bırakılacak (bunu açıklayan bir not yaz).
10. **Güvenlik & Rate Limiting**:
- Django password validators aktif olacak.
- Auth endpointleri için basic throttling / rate limit ayarları öner ve uygula (ör: DRF throttling).
11. **Testler**:
- En azından aşağıdakiler için tests yaz:
- Register → user oluşturuluyor mu, `is_active=False` mi, email aktivasyonu tetikleniyor mu?
- Activate → doğru uid/token ile aktivasyon başarılı mı? Yanlış ise hata veriyor mu?
- Login → aktif kullanıcı login olabiliyor mu? Aktif olmayan için hata dönüyor mu?
- Social login → access_token üzerinden providerdan user elde ediliyor mu, `is_active=True` ile user oluşturuluyor mu, JWT dönüyor mu?
12. **Dokümantasyon**:
- Örnek bir doküman dosyası: `AUTH.md` veya `README.md` içinde bir bölüm:
- Tüm auth endpointleri
- Örnek request/responselar
- Nuxt/Next tarafında nasıl kullanılacağına dair ayrintili notlar
- ENV değişkenleri (email, social provider keys vs.)
## Dosya ve Kod Yapı Planı
Aşağıdakiler bir referans plan; proje yapısına göre isimler uyarlanabilir (örn. `config/settings.py` vs. `project/settings.py`).
- `accounts/models.py`
- `CustomUser(AbstractBaseUser, PermissionsMixin)`:
- Alanlar: `email`, `first_name`, `last_name`, `is_staff`, `is_active`, `date_joined` vb.
- `USERNAME_FIELD = "email"`
- `REQUIRED_FIELDS = []` (veya first_name/last_name)
- `CustomUserManager`:
- `create_user`
- `create_superuser`
- `accounts/admin.py`
- Django adminde custom userı `UserAdmin` türeterek kaydet.
- `accounts/serializers.py`
- `CustomUserCreateSerializer`:
- Register sırasında kullanılır.
- `is_active=False` set eder.
- `CustomUserSerializer`:
- Kullanıcı profilini döner (id, email, first_name, last_name vb.).
- `SocialLoginSerializer`:
- Alanlar: `provider`, `access_token` (ve gerekiyorsa `id_token` vs.).
- `accounts/views.py`
- Gerekirse:
- `SocialLoginView` (providera göre token doğrulayan, user oluşturan/döndüren, sonra JWT üreten).
- İsteğe bağlı: `ResendActivationView`.
- `accounts/urls.py`
- `/auth/social/<provider>/` için route.
- Djoser urlleri (register/activate/jwt vb.) de burada veya ana `urls.py` içinde include edilebilir.
- `config/settings.py` (veya proje settings dosyası)
- `INSTALLED_APPS`:
- `'accounts'`
- `'rest_framework'`
- `'djoser'`
- `'rest_framework_simplejwt'`
- `'corsheaders'`
- Social auth için: `'social_django'` veya seçtiğimiz paket.
- `AUTH_USER_MODEL = "accounts.CustomUser"`
- `REST_FRAMEWORK` ayarları (`DEFAULT_AUTHENTICATION_CLASSES`, throttling vb.)
- `SIMPLE_JWT` ayarları
- `DJOSER` konfigürasyonu
- `CORS_ALLOWED_ORIGINS`
- `EMAIL_BACKEND` ve diğer email ayarları
- Social provider ayarları (`SOCIAL_AUTH_*` veya ilgili konfigler)
- `templates/email/activation_email.html`
- `templates/email/activation_email.txt`
- `requirements.txt`
- `Django==6.0`
- `djangorestframework==3.16.1`
- `djoser==2.3.3`
- `djangorestframework_simplejwt==5.5.1`
- `social-auth-app-django` (veya tercih ettiğimiz social auth paketi)
- `django-cors-headers`
- `tests/test_accounts.py`
- Yukarıda bahsettiğim tüm kritik akışlar için testler.
## Hafıza / Progress Kaydı: COPILOT_MEMORY.md
Projede ne yapıldığını **hatırlamak** için özel bir dosya istiyorum:
- Repo kökünde: `COPILOT_MEMORY.md`
- Copilot, her önemli adım veya mantıklı grup değişiklikten sonra bu dosyaya **append** yapsın.
- Kayıt formatı şöyle olsun (örnek):
```markdown
## 2025-12-11T14:32:00Z
- Değişiklik özeti:
- CustomUser model ve manager eklendi.
- settings.py'de AUTH_USER_MODEL ve djoser/jwt ayarlarının temeli kuruldu.
- Dosyalar:
- accounts/models.py
- accounts/admin.py
- config/settings.py
- Next steps:
- Custom registration serializer ve activation email şablonlarını ekle.
```
Kurallar:
- Her anlamlı değişiklik setinden sonra yeni bir `## <ISO 8601 timestamp>` başlığı ile blok ekle.
- Bir sonraki yapılacakları (Next steps) kısa ve net yaz.
- Eğer PR açıyorsan, ilgili PR açıklamasına bu kaydın kısa özetini de ekle.
## Geliştirme Adımları (Önerilen Sıra)
Copilottan beklediğim ilerleme sırası:
1. **Custom User Model**
- `accounts` app oluştur (yoksa).
- `CustomUser` ve `CustomUserManager` yaz.
- `AUTH_USER_MODEL` ayarla.
- Migrasyonları oluştur ve çalıştırılabilir hale getir (kod olarak migration dosyaları üret).
2. **Temel Settings Konfigürasyonu**
- `INSTALLED_APPS`, `REST_FRAMEWORK`, `SIMPLE_JWT`, `DJOSER`, `CORS`, `EMAIL_BACKEND` vs. temel ayarlar.
- Nuxt/Next için örnek CORS domainleri koy (yorum satırı olarak da olabilir).
3. **Djoser Register / Activate / JWT**
- Djoserın default endpointlerini aktif et.
- `CustomUserCreateSerializer`ı bağla, register sırasında `is_active=False` olsun.
- Aktivasyon URLini Nuxt/Next kullanacak şekilde düzenle.
4. **Social Auth Entegrasyonu**
- Seçilecek social auth paketini netleştir (preferans: `social-auth-app-django`).
- Provider konfigleri için env örnekleri yaz.
- `SocialLoginView` ve `SocialLoginSerializer` ile:
- Provider token doğrulama
- Email ile user bul/oluştur
- `is_active=True` set et
- JWT üret ve dön.
5. **Email Şablonları**
- Aktivasyon e-postası için HTML ve text templateleri yaz.
6. **Testler**
- Temel testler: register, activate, login, social login.
7. **Dokümantasyon**
- `AUTH.md` veya READMEye auth bölümünü ekle.
8. **COPILOT_MEMORY.md Güncellemeleri**
- Her adımda bu dosyaya not düş.
## Copilota Komutum
Bu promptu aldıktan sonra, lütfen:
1. Projeyi ve hedefleri özetle.
2. Social auth kütüphanesi seçimi için bana 12 öneri sun (örnek: `social-auth-app-django` vs `django-allauth`), artılarını/eksilerini kısaca yaz ve **hangisini seçmek istediğimi sor**.
3. Ben seçimi yaptıktan sonra şu adımla başla:
- CustomUser model ve managerı yaz.
- `settings.py` içinde gerekli temel konfigürasyonları ekle (AUTH_USER_MODEL, Djoser, DRF, JWT, CORS, EMAIL).
- `COPILOT_MEMORY.md` dosyasına ilk kaydı ekle.
4. Her büyük adım sonunda:
- Hangi dosyaları değiştirdiğini kısaca özetle.
- `COPILOT_MEMORY.md`ye uygun formatta giriş ekle.
- Bir sonraki adımı bana öner ve onayımı bekle (veya devam etmemi iste).
Başlangıç komutum:
> **Başla**:
> - CustomUser model ve manager oluştur.
> - `settings.py` gerekli auth (Djoser + JWT + DRF + CORS + EMAIL) konfigürasyonlarını ekle.
> - Djoserın register/activate/jwt endpointlerini temel haliyle ayağa kaldıracak url ve ayarları hazırla.
> - `COPILOT_MEMORY.md` dosyasına başlangıç girdisini ekle.
Bu promptu anladıysan, önce kısaca özetle, sonra social auth kütüphane tercihi için benden seçim iste ve ardından adım 1e başla.

9
core/Permission.py Normal file
View File

@@ -0,0 +1,9 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
class ReadOnly(BasePermission):
"""
Yalnızca okuma işlemlerine izin verir.
"""
def has_permission(self, request, view):
# SAFE_METHODS: ('GET', 'HEAD', 'OPTIONS')
return request.method in SAFE_METHODS

5
core/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
# Celery app'i import et, böylece Django başladığında her zaman yüklenir
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()

19
core/celery.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from celery import Celery
# Django settings modülünü ayarla
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
app = Celery('core')
# Django settings'ten celery yapılandırmasını yükle
app.config_from_object('django.conf:settings', namespace='CELERY')
# Tüm Django app'lerden tasks.py dosyalarını otomatik olarak keşfet
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')

481
core/settings.py Normal file
View File

@@ -0,0 +1,481 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 6.0.
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/
"""
from pathlib import Path
import os
from dotenv import load_dotenv
# .env dosyasını yükle
load_dotenv()
# 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 = 'django-insecure-g0y6e(tw41ugb6y73&do6#12+)qk1gh5o+s2v8_m1c5rrq4i3e'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', False)
ALLOWED_HOSTS = ['back.beyhan.gen.tr', 'localhost', '127.0.0.1', '[::1]']
CSRF_TRUSTED_ORIGINS = ['https://back.beyhan.gen.tr', 'https://*.beyhan.gen.tr']
CSRF_COOKIE_DOMAIN = '.beyhan.gen.tr'
# Site URL - .env dosyasından alınır
SITE_URL = os.getenv('SITE_URL', 'http://127.0.0.1:8000')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3. Parti
'rest_framework',
'rest_framework_simplejwt',
'django_celery_results',
# celery -A [project-name] worker --beat --scheduler django --loglevel=info
'django_celery_beat',
'djoser',
'corsheaders',
'social_django',
'imagekit',
'django_cleanup',
'colorfield',
'autoslug',
'tinymce',
"debug_toolbar",
'django.contrib.sites', # Added for Djoser domain resolution
# Local Apps
'accounts',
'settings',
'backup',
'contact',
'product',
'cart',
'reviews',
'blog',
]
SITE_ID = 1
CART_SESSION_ID = 'cart'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'accounts.middleware.SocialAuthExceptionMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
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',
# Social auth context processors
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
'cart.context_processors.cart',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
# Docker ortamında veya environment değişkeni varsa PostgreSQL kullan
USE_POSTGRES = os.getenv('USE_POSTGRES', 'True').lower() == 'true'
if USE_POSTGRES:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('POSTGRES_DB', 'shop'),
'USER': os.getenv('POSTGRES_USER', 'shop'),
'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'gg7678290'),
'HOST': os.getenv('POSTGRES_HOST', '212.64.215.243'),
'PORT': os.getenv('POSTGRES_PORT', '5432'),
'OPTIONS': {
'options': '-c search_path=public'
},
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# ==============================================================================
# REDIS CACHE CONFIGURATION
# ==============================================================================
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
# 'LOCATION': 'redis://default:1923btO**@ares-redis-xrot7z:6379',
'LOCATION': os.getenv('CELERY_BROKER_URL', 'redis://default:8KNa2T3ceGkrYPpt@212.64.215.243:6379/3'),
# 'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'ata_',
'TIMEOUT': 300, # 5 dakika default timeout
}
}
# ==============================================================================
# CELERY CONFIGURATION
# ==============================================================================
# CELERY_BROKER_URL = 'redis://default:1923btO**@10.80.80.70:6379/5'
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://default:8KNa2T3ceGkrYPpt@212.64.215.243:6379/5')
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'django-db')
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Istanbul' # Türkiye timezone
CELERY_ENABLE_UTC = True # UTC zaman kullan
# 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 = 'tr'
TIME_ZONE = 'Europe/Istanbul'
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'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ==============================================================================
# CUSTOM USER MODEL
# ==============================================================================
AUTH_USER_MODEL = 'accounts.CustomUser'
# ==============================================================================
# REST FRAMEWORK CONFIGURATION
# ==============================================================================
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication', # For social auth
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
# Throttling for security - api.denizogur.com.tr için bypass edildi
'DEFAULT_THROTTLE_CLASSES': [
'core.throttling.CustomAnonRateThrottle',
'core.throttling.CustomUserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour', # Anonymous users
'user': '1000/hour', # Authenticated users
},
}
# ==============================================================================
# SIMPLE JWT CONFIGURATION
# ==============================================================================
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=120),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
}
# ==============================================================================
# DJOSER CONFIGURATION
# ==============================================================================
DJOSER = {
# Domain for email links (YOUR FRONTEND URL)
# Djoser combines DOMAIN + ACTIVATION_URL to create the full link
'DOMAIN': 'localhost:3000', # IMPORTANT: Change this to your frontend's domain
'SITE_NAME': 'Django Auth API',
# Registration & Activation
'SEND_ACTIVATION_EMAIL': True,
'ACTIVATION_URL': 'activate/{uid}/{token}', # Frontend route, e.g., http://localhost:3000/activate/MQ/token/
# Password Reset
'SEND_CONFIRMATION_EMAIL': True,
'PASSWORD_RESET_CONFIRM_URL': 'password-reset/{uid}/{token}', # Frontend route
'PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND': False,
# Username Reset
'USERNAME_RESET_CONFIRM_URL': 'username-reset/{uid}/{token}', # Frontend route
# Email confirmations
'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True,
'USERNAME_CHANGED_EMAIL_CONFIRMATION': True,
# User settings
'USER_CREATE_PASSWORD_RETYPE': True,
'SET_PASSWORD_RETYPE': True,
'PASSWORD_RESET_CONFIRM_RETYPE': True,
'LOGIN_FIELD': 'email',
# Serializers
'SERIALIZERS': {
'user_create': 'accounts.serializers.CustomUserCreateSerializer',
'user': 'accounts.serializers.CustomUserSerializer',
'current_user': 'accounts.serializers.CustomUserSerializer',
},
}
# ==============================================================================
# EMAIL CONFIGURATION
# ==============================================================================
# Development: Using MailPit (local email testing tool)
# MailPit default runs on localhost:1025 for SMTP and localhost:8025 for web UI
# SITE_URL = os.getenv('SITE_URL', 'http://127.0.0.1:8000')
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND','django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = os.getenv('EMAIL_HOST','212.64.215.243')
EMAIL_PORT = os.getenv('EMAIL_PORT',1025)
# EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS',False)
# EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL',True)
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER','')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD','')
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL','noreply@localhost')
# Production: Uncomment and configure these with environment variables
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
# EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
# EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'True') == 'True'
# EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
# EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
# DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@yourdomain.com')
# ==============================================================================
# CORS CONFIGURATION
# ==============================================================================
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # Next.js default
"http://127.0.0.1:3000",
"http://localhost:5173", # Vite default
"http://127.0.0.1:5173",
"http://localhost:8080", # Vue/Nuxt alternative port
"http://127.0.0.1:8080",
]
# For development only - be careful in production!
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
# ==============================================================================
# SOCIAL AUTH CONFIGURATION (Python Social Auth)
# ==============================================================================
AUTHENTICATION_BACKENDS = (
# Social auth backends
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'social_core.backends.facebook.FacebookOAuth2',
# Add more providers as needed
# Django default
'django.contrib.auth.backends.ModelBackend',
)
# Social Auth Settings
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_URL_NAMESPACE = 'social'
# Pipeline - custom pipeline to set is_active=True for social users
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'accounts.pipeline.activate_user', # Custom pipeline to set is_active=True
)
# User model
SOCIAL_AUTH_USER_MODEL = 'accounts.CustomUser'
SOCIAL_AUTH_USERNAME_IS_REQUIRED = False
SOCIAL_AUTH_USER_FIELDS = ['email', 'first_name', 'last_name']
# Strategy
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
# Google OAuth2 Configuration
# Get credentials from: https://console.developers.google.com/
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '915364976256-691m0s87as2r5vdbqr96f6humblseobt.apps.googleusercontent.com' # Your Google Client ID
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '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' # Your Google Client Secret
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
SOCIAL_AUTH_GOOGLE_OAUTH2_EXTRA_DATA = ['first_name', 'last_name']
# GitHub OAuth2 Configuration
# Get credentials from: https://github.com/settings/developers
SOCIAL_AUTH_GITHUB_KEY = 'Ov23liUt9B61O46Mdfm4' # Your GitHub Client ID
SOCIAL_AUTH_GITHUB_SECRET = 'c7fc8dcb1b2c8f22120608425d07d5efd995baaf' # Your GitHub Client Secret
SOCIAL_AUTH_GITHUB_SCOPE = ['user:email']
# Facebook OAuth2 Configuration
# Get credentials from: https://developers.facebook.com/
SOCIAL_AUTH_FACEBOOK_KEY = '' # Your Facebook App ID
SOCIAL_AUTH_FACEBOOK_SECRET = '' # Your Facebook App Secret
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id, name, email, first_name, last_name'
}
# Redirect URLs (customize for your frontend)
LOGIN_URL = '/api/v1/spa/'
LOGIN_REDIRECT_URL = '/api/v1/auth/social/callback/'
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/api/v1/auth/social/callback/'
SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/api/v1/auth/social/callback/'
SOCIAL_AUTH_INACTIVE_USER_URL = '/api/v1/auth/social/error/'
SOCIAL_AUTH_LOGIN_ERROR_URL = '/api/v1/auth/social/error/'
TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}
# ==============================================================================
# SECURITY SETTINGS FOR SPA/JWT
# ==============================================================================
# Since we're using JWT tokens (not session cookies), we can relax CSRF for API endpoints
# But keep it enabled for Django admin
"""
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SECURE = False # Set to True in production with HTTPS
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000",
"http://localhost:5173",
]
"""
INTERNAL_IPS = [
"127.0.0.1",
]
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.history.HistoryPanel',
'debug_toolbar.panels.versions.VersionsPanel',
'debug_toolbar.panels.timer.TimerPanel',
'debug_toolbar.panels.settings.SettingsPanel',
'debug_toolbar.panels.headers.HeadersPanel',
'debug_toolbar.panels.request.RequestPanel',
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
'debug_toolbar.panels.alerts.AlertsPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.community.CommunityPanel',
# 'debug_toolbar.panels.redirects.RedirectsPanel', # Deprecated olduğu için kaldırıldı
'debug_toolbar.panels.profiling.ProfilingPanel',
]
DEBUG_TOOLBAR_CONFIG = {
'INTERCEPT_REDIRECTS': False,
}

350
core/throttling.py Normal file
View File

@@ -0,0 +1,350 @@
"""
Custom throttling classes for API rate limiting
"""
import logging
import ipaddress
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
# Logger yapılandırması
logger = logging.getLogger('throttling')
logger.setLevel(logging.INFO)
# File handler ekle (logs dizinine yaz)
import os
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOG_DIR = BASE_DIR / 'logs'
LOG_DIR.mkdir(exist_ok=True)
# Formatter oluştur
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# File handler oluştur (dosyaya yaz)
file_handler = logging.FileHandler(LOG_DIR / 'throttling.log', encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
# Stream handler oluştur (konsola yaz - Docker için)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# Handler'ları logger'a ekle (duplicate handler'ları önlemek için)
if not logger.handlers:
logger.addHandler(file_handler)
logger.addHandler(console_handler)
def is_cloudflare_ip(ip):
"""
IP adresinin Cloudflare IP aralığında olup olmadığını kontrol eder
Cloudflare IP aralıkları: https://www.cloudflare.com/ips/
"""
try:
ip_obj = ipaddress.ip_address(ip)
# Cloudflare IPv4 aralıkları (en yaygın olanlar)
cloudflare_ranges = [
ipaddress.ip_network('173.245.48.0/20'),
ipaddress.ip_network('103.21.244.0/22'),
ipaddress.ip_network('103.22.200.0/22'),
ipaddress.ip_network('103.31.4.0/22'),
ipaddress.ip_network('141.101.64.0/18'),
ipaddress.ip_network('108.162.192.0/18'),
ipaddress.ip_network('190.93.240.0/20'),
ipaddress.ip_network('188.114.96.0/20'),
ipaddress.ip_network('197.234.240.0/22'),
ipaddress.ip_network('198.41.128.0/17'),
ipaddress.ip_network('162.158.0.0/15'),
ipaddress.ip_network('104.16.0.0/13'),
ipaddress.ip_network('104.24.0.0/14'),
ipaddress.ip_network('172.64.0.0/13'),
ipaddress.ip_network('131.0.72.0/22'),
]
# Cloudflare IPv6 aralıkları
cloudflare_ranges_v6 = [
ipaddress.ip_network('2400:cb00::/32'),
ipaddress.ip_network('2606:4700::/32'),
ipaddress.ip_network('2803:f800::/32'),
ipaddress.ip_network('2405:b500::/32'),
ipaddress.ip_network('2405:8100::/32'),
ipaddress.ip_network('2a06:98c0::/29'),
ipaddress.ip_network('2c0f:f248::/32'),
]
all_ranges = cloudflare_ranges + cloudflare_ranges_v6
return any(ip_obj in network for network in all_ranges)
except (ValueError, ipaddress.AddressValueError):
return False
class CustomAnonRateThrottle(AnonRateThrottle):
"""
Belirli IP'ler için throttling'i bypass eder
"""
EXEMPT_IPS = [
'127.0.0.1',
'localhost',
'::1',
'212.64.215.243',
'162.158.210.254',
'188.132.232.119',
]
def get_client_ip(self, request):
"""
Client IP adresini al
Cloudflare kullanıldığında CF-Connecting-IP header'ını öncelikli kullanır
"""
# Cloudflare gerçek client IP'si (en güvenilir)
cf_connecting_ip = request.META.get('HTTP_CF_CONNECTING_IP')
if cf_connecting_ip:
return cf_connecting_ip.strip()
# True-Client-IP (bazı Cloudflare yapılandırmalarında)
true_client_ip = request.META.get('HTTP_TRUE_CLIENT_IP')
if true_client_ip:
return true_client_ip.strip()
# X-Forwarded-For (fallback)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
# İlk IP genellikle gerçek client IP'sidir
ip = x_forwarded_for.split(',')[0].strip()
return ip
# Son çare: REMOTE_ADDR
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
# Eğer REMOTE_ADDR Cloudflare IP'si ise, gerçek IP bulunamadı demektir
if is_cloudflare_ip(remote_addr):
logger.warning(
f"[IP DETECTION] Cloudflare IP tespit edildi ({remote_addr}) "
f"ama gerçek client IP bulunamadı. CF-Connecting-IP header'ı eksik olabilir."
)
return remote_addr
def get_cache_key(self, request, view):
"""
Cache key'i gerçek client IP'ye göre oluşturur
Cloudflare kullanıldığında gerçek IP'yi kullanır
"""
# Gerçek client IP'yi al
ident = self.get_client_ip(request)
# Cache key format: throttle_anon_{ip}
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
def allow_request(self, request, view):
# Get client IP
ip = self.get_client_ip(request)
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
host = request.get_host().split(':')[0]
path = request.path
method = request.method
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
# View bilgisi
view_name = getattr(view, '__class__', None)
view_name = view_name.__name__ if view_name else 'unknown'
# Cloudflare kontrolü
is_from_cloudflare = is_cloudflare_ip(remote_addr)
cf_info = f"CF-IP: {remote_addr}" if is_from_cloudflare else ""
# Belirtilen IP'lerden geliyorsa throttling yapma
if ip in self.EXEMPT_IPS:
logger.info(
f"[ANON THROTTLE - BYPASS] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User-Agent: {user_agent[:100]}"
)
return True
# Normal throttling kurallarını uygula
allowed = super().allow_request(request, view)
# Throttle durumunu kontrol et
if allowed:
# Rate limit bilgilerini al
throttle_scope = getattr(view, 'throttle_scope', None) or 'anon'
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
# Cache key'den kalan istek sayısını tahmin et
cache_key = self.get_cache_key(request, view)
history = self.cache.get(cache_key, [])
remaining = max(0, num_requests - len(history))
logger.info(
f"[ANON THROTTLE - ALLOWED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"Rate: {rate} | Remaining: {remaining}/{num_requests} | "
f"User-Agent: {user_agent[:100]}"
)
else:
# Throttle limit aşıldı
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
logger.warning(
f"[ANON THROTTLE - BLOCKED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"Rate: {rate} | Limit: {num_requests}/{duration} | "
f"User-Agent: {user_agent[:100]}"
)
return allowed
class CustomUserRateThrottle(UserRateThrottle):
"""
Belirli kullanıcılar veya domainler için throttling'i bypass eder
"""
EXEMPT_HOSTS = [
'api.denizogur.com.tr',
'back.beyhan.gen.tr',
'shop.beyhan.gen.tr',
'beyhan.gen.tr',
'denizogur.com.tr',
'denizour.com.tr',
'localhost',
'127.0.0.1',
]
def get_client_ip(self, request):
"""
Client IP adresini al
Cloudflare kullanıldığında CF-Connecting-IP header'ını öncelikli kullanır
"""
# Cloudflare gerçek client IP'si (en güvenilir)
cf_connecting_ip = request.META.get('HTTP_CF_CONNECTING_IP')
if cf_connecting_ip:
return cf_connecting_ip.strip()
# True-Client-IP (bazı Cloudflare yapılandırmalarında)
true_client_ip = request.META.get('HTTP_TRUE_CLIENT_IP')
if true_client_ip:
return true_client_ip.strip()
# X-Forwarded-For (fallback)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
# İlk IP genellikle gerçek client IP'sidir
ip = x_forwarded_for.split(',')[0].strip()
return ip
# Son çare: REMOTE_ADDR
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
# Eğer REMOTE_ADDR Cloudflare IP'si ise, gerçek IP bulunamadı demektir
if is_cloudflare_ip(remote_addr):
logger.warning(
f"[IP DETECTION] Cloudflare IP tespit edildi ({remote_addr}) "
f"ama gerçek client IP bulunamadı. CF-Connecting-IP header'ı eksik olabilir."
)
return remote_addr
def get_cache_key(self, request, view):
"""
Cache key'i gerçek client IP'ye göre oluşturur
Cloudflare kullanıldığında gerçek IP'yi kullanır
UserRateThrottle için user ID de eklenir
"""
# Authenticated kullanıcı için user ID kullan
if request.user and request.user.is_authenticated:
ident = request.user.pk
else:
# Anonymous kullanıcı için gerçek client IP'yi kullan
ident = self.get_client_ip(request)
# Cache key format: throttle_user_{user_id} veya throttle_anon_{ip}
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
def allow_request(self, request, view):
# Get client IP
ip = self.get_client_ip(request)
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
host = request.get_host().split(':')[0]
path = request.path
method = request.method
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
# View bilgisi
view_name = getattr(view, '__class__', None)
view_name = view_name.__name__ if view_name else 'unknown'
# User bilgisi
user_info = 'anonymous'
if request.user and request.user.is_authenticated:
user_info = f"user_id:{request.user.id} | email:{getattr(request.user, 'email', 'N/A')} | staff:{request.user.is_staff}"
# Cloudflare kontrolü
is_from_cloudflare = is_cloudflare_ip(remote_addr)
cf_info = f"CF-IP: {remote_addr}" if is_from_cloudflare else ""
# Host kontrolü
if host in self.EXEMPT_HOSTS:
logger.info(
f"[USER THROTTLE - BYPASS (HOST)] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | User-Agent: {user_agent[:100]}"
)
return True
# Authenticated kullanıcı için throttling yapma (staff users)
if request.user and request.user.is_authenticated and request.user.is_staff:
logger.info(
f"[USER THROTTLE - BYPASS (STAFF)] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | User-Agent: {user_agent[:100]}"
)
return True
# Normal throttling kurallarını uygula
allowed = super().allow_request(request, view)
# Throttle durumunu kontrol et
if allowed:
# Rate limit bilgilerini al
throttle_scope = getattr(view, 'throttle_scope', None) or 'user'
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
# Cache key'den kalan istek sayısını tahmin et
cache_key = self.get_cache_key(request, view)
history = self.cache.get(cache_key, [])
remaining = max(0, num_requests - len(history))
logger.info(
f"[USER THROTTLE - ALLOWED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | Rate: {rate} | Remaining: {remaining}/{num_requests} | "
f"User-Agent: {user_agent[:100]}"
)
else:
# Throttle limit aşıldı
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
logger.warning(
f"[USER THROTTLE - BLOCKED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | Rate: {rate} | Limit: {num_requests}/{duration} | "
f"User-Agent: {user_agent[:100]}"
)
return allowed

41
core/urls.py Normal file
View File

@@ -0,0 +1,41 @@
"""
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 debug_toolbar.toolbar import debug_toolbar_urls
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# Main API routes (includes SPA pages)
path('api/v1/', include('accounts.urls')),
path('api/v1/', include('settings.urls')),
path('api/v1/', include('contact.urls')),
path('api/v1/', include('product.urls')),
path('api/v1/blog/', include('blog.urls')),
path('api/v1/cart/', include('cart.urls', namespace='cart')),
path('api/v1/reviews/', include('reviews.urls')), # Reviews URL'leri eklendi
path('api/v1/auth/', include('djoser.urls.jwt')),
]+ debug_toolbar_urls()
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)

Some files were not shown because too many files have changed in this diff Show More