Initial commit: Django backend with Campaign, ContactUs, and Composition APIs

This commit is contained in:
Shayan Azadi
2025-12-17 15:58:07 +03:30
commit 3e100bae99
23 changed files with 1454 additions and 0 deletions

51
.gitignore vendored Normal file
View File

@@ -0,0 +1,51 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
/media
/staticfiles
# Virtual Environment
venv/
env/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Environment variables
.env
.env.local

135
README.md Normal file
View File

@@ -0,0 +1,135 @@
# Zoneco ORG Backend API
A professional Django REST Framework backend for Zoneco ORG with PostgreSQL database.
## Features
- **Contact Us API**: Handle contact form submissions with name, email/phone, description, and category
- **Composition API**: Manage compositions with name, description, and image
- **Campaign API**: Manage campaigns with name, description, start/end times, and image
- **RESTful API**: Full CRUD operations for all models
- **CORS Enabled**: Configured for frontend integration
- **Image Upload**: Support for image uploads with proper media handling
- **Admin Panel**: Django admin interface for managing data
## Database Configuration
- **Database Name**: Zoneco_ORG
- **User**: postgres
- **Password**: postgres
- **Host**: localhost
- **Port**: 5432
## Installation
1. **Install Python dependencies:**
```bash
pip install -r requirements.txt
```
2. **Create PostgreSQL database:**
```sql
CREATE DATABASE "Zoneco_ORG";
```
3. **Run migrations:**
```bash
python manage.py makemigrations
python manage.py migrate
```
4. **Create superuser (optional):**
```bash
python manage.py createsuperuser
```
5. **Run development server:**
```bash
python manage.py runserver
```
The API will be available at `http://localhost:8000/api/`
## API Endpoints
### Contact Us
- `GET /api/contact-us/` - List all contacts
- `POST /api/contact-us/` - Create new contact (public)
- `GET /api/contact-us/{id}/` - Get specific contact
- `PUT /api/contact-us/{id}/` - Update contact (authenticated)
- `PATCH /api/contact-us/{id}/` - Partial update (authenticated)
- `DELETE /api/contact-us/{id}/` - Delete contact (authenticated)
- `GET /api/contact-us/by_category/?category={category}` - Filter by category
### Compositions
- `GET /api/compositions/` - List all compositions
- `POST /api/compositions/` - Create new composition (authenticated)
- `GET /api/compositions/{id}/` - Get specific composition
- `PUT /api/compositions/{id}/` - Update composition (authenticated)
- `PATCH /api/compositions/{id}/` - Partial update (authenticated)
- `DELETE /api/compositions/{id}/` - Delete composition (authenticated)
### Campaigns
- `GET /api/campaigns/` - List all campaigns
- `POST /api/campaigns/` - Create new campaign (authenticated)
- `GET /api/campaigns/{id}/` - Get specific campaign
- `PUT /api/campaigns/{id}/` - Update campaign (authenticated)
- `PATCH /api/campaigns/{id}/` - Partial update (authenticated)
- `DELETE /api/campaigns/{id}/` - Delete campaign (authenticated)
- `GET /api/campaigns/active/` - Get active campaigns
- `GET /api/campaigns/upcoming/` - Get upcoming campaigns
- `GET /api/campaigns/ended/` - Get ended campaigns
## Models
### ContactUs
- `name` (CharField): Name of the contact
- `email_or_phone` (CharField): Email or phone number
- `description` (TextField): Message description
- `category` (CharField): Category (همکاری, فروش, پشتیبانی, درخواست مشاور, سایر)
- `created_at` (DateTimeField): Creation timestamp
- `updated_at` (DateTimeField): Last update timestamp
### Composition
- `name` (CharField): Name of the composition
- `description` (TextField): Description
- `image` (ImageField): Composition image
- `created_at` (DateTimeField): Creation timestamp
- `updated_at` (DateTimeField): Last update timestamp
### Campaign
- `name` (CharField): Campaign name
- `description` (TextField): Campaign description
- `start_time` (DateTimeField): Campaign start time
- `end_time` (DateTimeField): Campaign end time
- `image` (ImageField): Campaign image
- `created_at` (DateTimeField): Creation timestamp
- `updated_at` (DateTimeField): Last update timestamp
## Admin Panel
Access the Django admin panel at `http://localhost:8000/admin/` after creating a superuser.
## CORS Configuration
CORS is configured to allow requests from:
- `http://localhost:5173` (Vite default)
- `http://localhost:3000`
- `http://127.0.0.1:5173`
- `http://127.0.0.1:3000`
## Media Files
Uploaded images are stored in the `media/` directory:
- Composition images: `media/compositions/`
- Campaign images: `media/campaigns/`
## Development Notes
- The backend uses Django REST Framework for API endpoints
- PostgreSQL is used as the database
- Image uploads are handled via multipart/form-data
- All models include proper indexing for performance
- Serializers include validation and proper error handling
- Viewsets provide both public read access and authenticated write access

View File

@@ -0,0 +1,352 @@
{
"info": {
"_postman_id": "zoneco-org-api-collection",
"name": "Zoneco ORG API",
"description": "Complete API collection for Zoneco ORG backend - Campaign, Contact Us, and Composition endpoints",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Campaigns",
"item": [
{
"name": "GET All Campaigns",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/campaigns/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"campaigns",
""
]
},
"description": "Get all campaigns"
},
"response": []
},
{
"name": "GET Campaign by ID",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/campaigns/1/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"campaigns",
"1",
""
]
},
"description": "Get a specific campaign by ID"
},
"response": []
},
{
"name": "POST Create Campaign",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"کمپین تست\",\n \"description\": \"این یک کمپین تستی است\",\n \"start_time\": \"2024-01-01T00:00:00Z\",\n \"end_time\": \"2024-12-31T23:59:59Z\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/campaigns/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"campaigns",
""
]
},
"description": "Create a new campaign"
},
"response": []
},
{
"name": "GET Active Campaigns",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/campaigns/active/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"campaigns",
"active",
""
]
},
"description": "Get all currently active campaigns"
},
"response": []
},
{
"name": "GET Upcoming Campaigns",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/campaigns/upcoming/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"campaigns",
"upcoming",
""
]
},
"description": "Get all upcoming campaigns"
},
"response": []
},
{
"name": "GET Ended Campaigns",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/campaigns/ended/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"campaigns",
"ended",
""
]
},
"description": "Get all ended campaigns"
},
"response": []
}
],
"description": "Campaign management endpoints"
},
{
"name": "Contact Us",
"item": [
{
"name": "GET All Contact Us",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/contact-us/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"contact-us",
""
]
},
"description": "Get all contact form submissions"
},
"response": []
},
{
"name": "GET Contact Us by ID",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/contact-us/1/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"contact-us",
"1",
""
]
},
"description": "Get a specific contact submission by ID"
},
"response": []
},
{
"name": "POST Create Contact Us",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"احمد محمدی\",\n \"email_or_phone\": \"ahmad@example.com\",\n \"description\": \"سلام، می‌خواستم در مورد خدمات شما اطلاعات بیشتری دریافت کنم.\",\n \"category\": \"پشتیبانی\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/contact-us/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"contact-us",
""
]
},
"description": "Submit a new contact form"
},
"response": []
},
{
"name": "GET Contact Us by Category",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/contact-us/by_category/?category=پشتیبانی",
"host": [
"{{base_url}}"
],
"path": [
"api",
"contact-us",
"by_category",
""
],
"query": [
{
"key": "category",
"value": "پشتیبانی",
"description": "Category: همکاری, فروش, پشتیبانی, درخواست مشاور, سایر"
}
]
},
"description": "Get contacts filtered by category"
},
"response": []
}
],
"description": "Contact form submission endpoints"
},
{
"name": "Compositions",
"item": [
{
"name": "GET All Compositions",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/compositions/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"compositions",
""
]
},
"description": "Get all compositions"
},
"response": []
},
{
"name": "GET Composition by ID",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/compositions/1/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"compositions",
"1",
""
]
},
"description": "Get a specific composition by ID"
},
"response": []
},
{
"name": "POST Create Composition",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"ترکیب تست\",\n \"description\": \"این یک ترکیب تستی است\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{base_url}}/api/compositions/",
"host": [
"{{base_url}}"
],
"path": [
"api",
"compositions",
""
]
},
"description": "Create a new composition"
},
"response": []
}
],
"description": "Composition management endpoints"
}
],
"variable": [
{
"key": "base_url",
"value": "http://localhost:8000",
"type": "string"
}
]
}

0
api/__init__.py Normal file
View File

72
api/admin.py Normal file
View File

@@ -0,0 +1,72 @@
from django.contrib import admin
from .models import ContactUs, Composition, Campaign
@admin.register(ContactUs)
class ContactUsAdmin(admin.ModelAdmin):
list_display = ['name', 'email_or_phone', 'category', 'created_at']
list_filter = ['category', 'created_at']
search_fields = ['name', 'email_or_phone', 'description']
readonly_fields = ['created_at', 'updated_at']
date_hierarchy = 'created_at'
fieldsets = (
('اطلاعات تماس', {
'fields': ('name', 'email_or_phone', 'category')
}),
('پیام', {
'fields': ('description',)
}),
('اطلاعات زمانی', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(Composition)
class CompositionAdmin(admin.ModelAdmin):
list_display = ['name', 'created_at']
search_fields = ['name', 'description']
readonly_fields = ['created_at', 'updated_at']
date_hierarchy = 'created_at'
fieldsets = (
('اطلاعات ترکیب', {
'fields': ('name', 'description', 'image')
}),
('اطلاعات زمانی', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(Campaign)
class CampaignAdmin(admin.ModelAdmin):
list_display = ['name', 'start_time', 'end_time', 'is_active', 'created_at']
list_filter = ['start_time', 'end_time', 'created_at']
search_fields = ['name', 'description']
readonly_fields = ['created_at', 'updated_at', 'is_active']
date_hierarchy = 'start_time'
fieldsets = (
('اطلاعات کمپین', {
'fields': ('name', 'description', 'image')
}),
('زمان‌بندی', {
'fields': ('start_time', 'end_time')
}),
('وضعیت', {
'fields': ('is_active',)
}),
('اطلاعات زمانی', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def is_active(self, obj):
return obj.is_active()
is_active.boolean = True
is_active.short_description = 'فعال'

6
api/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

View File

View File

View File

@@ -0,0 +1,125 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from api.models import ContactUs, Composition, Campaign
class Command(BaseCommand):
help = 'Seed the database with Persian sample data'
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Starting to seed database with Persian data...'))
# Seed ContactUs data
self.stdout.write('Creating ContactUs records...')
contact_data = [
{
'name': 'احمد محمدی',
'email_or_phone': 'ahmad.mohammadi@example.com',
'description': 'سلام، من علاقه‌مند به همکاری با شرکت شما هستم. لطفاً اطلاعات بیشتری در مورد فرصت‌های شغلی ارسال کنید.',
'category': 'همکاری'
},
{
'name': 'فاطمه رضایی',
'email_or_phone': '09123456789',
'description': 'با سلام، می‌خواستم در مورد محصولات شما اطلاعات بیشتری دریافت کنم. آیا امکان بازدید از نمایشگاه وجود دارد؟',
'category': 'فروش'
},
{
'name': 'علی کریمی',
'email_or_phone': 'ali.karimi@example.com',
'description': 'سلام، من مشکلی در استفاده از سرویس شما دارم. لطفاً راهنمایی کنید.',
'category': 'پشتیبانی'
},
{
'name': 'زهرا احمدی',
'email_or_phone': '09187654321',
'description': 'با احترام، می‌خواستم در مورد پروژه‌های سرمایه‌گذاری شما مشاوره دریافت کنم. لطفاً با من تماس بگیرید.',
'category': 'درخواست مشاور'
},
{
'name': 'محمد حسینی',
'email_or_phone': 'mohammad.hosseini@example.com',
'description': 'سلام، سوالی در مورد خدمات شما دارم که در دسته‌بندی‌های موجود نیست. لطفاً راهنمایی کنید.',
'category': 'سایر'
}
]
for i, data in enumerate(contact_data, 1):
ContactUs.objects.create(**data)
self.stdout.write(f' Created contact {i}/5')
# Seed Composition data
self.stdout.write('Creating Composition records...')
composition_data = [
{
'name': 'ترکیب استراتژیک زونکو',
'description': 'این ترکیب شامل بخش‌های مختلف شرکت زونکو می‌باشد که شامل مدیریت، توسعه، بازاریابی و فروش است. این ترکیب به گونه‌ای طراحی شده که بتواند حداکثر کارایی را در تمامی بخش‌ها ایجاد کند.'
},
{
'name': 'ترکیب تکنولوژی و نوآوری',
'description': 'ترکیبی از تکنولوژی‌های پیشرفته و نوآوری‌های روز دنیا که در شرکت زونکو به کار گرفته می‌شود. این ترکیب شامل هوش مصنوعی، بلاک چین و سایر تکنولوژی‌های نوین است.'
},
{
'name': 'ترکیب منابع انسانی',
'description': 'ترکیب بهینه از نیروی انسانی متخصص و با تجربه که در بخش‌های مختلف شرکت فعالیت می‌کنند. این ترکیب شامل مدیران ارشد، متخصصان فنی و کارشناسان بازاریابی است.'
},
{
'name': 'ترکیب مالی و سرمایه‌گذاری',
'description': 'ترکیب استراتژیک از منابع مالی و فرصت‌های سرمایه‌گذاری که شرکت زونکو در اختیار دارد. این ترکیب شامل سرمایه‌گذاری‌های داخلی و خارجی و مدیریت ریسک است.'
},
{
'name': 'ترکیب بازاریابی و فروش',
'description': 'ترکیب موثر از استراتژی‌های بازاریابی و فروش که برای دستیابی به اهداف تجاری شرکت طراحی شده است. این ترکیب شامل بازاریابی دیجیتال، روابط عمومی و فروش مستقیم است.'
}
]
for i, data in enumerate(composition_data, 1):
Composition.objects.create(**data)
self.stdout.write(f' Created composition {i}/5')
# Seed Campaign data
self.stdout.write('Creating Campaign records...')
now = timezone.now()
campaign_data = [
{
'name': 'کمپین تابستانه زونکو ۱۴۰۳',
'description': 'کمپین ویژه تابستان ۱۴۰۳ با تخفیف‌های ویژه و جوایز نقدی. این کمپین شامل تمامی محصولات و خدمات شرکت می‌باشد و فرصت مناسبی برای مشتریان است.',
'start_time': now - timedelta(days=30),
'end_time': now + timedelta(days=30)
},
{
'name': 'کمپین همکاری استراتژیک',
'description': 'کمپین جذب شرکای تجاری جدید برای همکاری در پروژه‌های بزرگ. این کمپین فرصت مناسبی برای شرکت‌هایی است که می‌خواهند با زونکو همکاری کنند.',
'start_time': now - timedelta(days=15),
'end_time': now + timedelta(days=45)
},
{
'name': 'کمپین نوآوری و تکنولوژی',
'description': 'کمپین معرفی آخرین نوآوری‌ها و تکنولوژی‌های شرکت زونکو. این کمپین شامل نمایشگاه‌ها، سمینارها و رویدادهای تخصصی است.',
'start_time': now + timedelta(days=10),
'end_time': now + timedelta(days=70)
},
{
'name': 'کمپین سرمایه‌گذاری زونکو',
'description': 'کمپین جذب سرمایه‌گذاران برای پروژه‌های جدید شرکت. این کمپین شامل ارائه اطلاعات کامل در مورد فرصت‌های سرمایه‌گذاری و بازدهی مورد انتظار است.',
'start_time': now - timedelta(days=60),
'end_time': now - timedelta(days=10)
},
{
'name': 'کمپین آموزش و توسعه',
'description': 'کمپین ارائه دوره‌های آموزشی و کارگاه‌های تخصصی برای توسعه مهارت‌های حرفه‌ای. این کمپین شامل دوره‌های آنلاین و حضوری است.',
'start_time': now + timedelta(days=5),
'end_time': now + timedelta(days=65)
}
]
for i, data in enumerate(campaign_data, 1):
Campaign.objects.create(**data)
self.stdout.write(f' Created campaign {i}/5')
self.stdout.write(self.style.SUCCESS('\nSuccessfully seeded database with Persian data!'))
self.stdout.write(f' - ContactUs: {ContactUs.objects.count()} records')
self.stdout.write(f' - Composition: {Composition.objects.count()} records')
self.stdout.write(f' - Campaign: {Campaign.objects.count()} records')

View File

@@ -0,0 +1,68 @@
# Generated by Django 4.2.7 on 2025-12-17 12:13
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ContactUs',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='نام و نام خانوادگی')),
('email_or_phone', models.CharField(help_text='Can be either email or phone number', max_length=200, verbose_name='ایمیل / شماره تماس')),
('description', models.TextField(verbose_name='توضیحات')),
('category', models.CharField(choices=[('همکاری', 'همکاری'), ('فروش', 'فروش'), ('پشتیبانی', 'پشتیبانی'), ('درخواست مشاور', 'درخواست مشاور'), ('سایر', 'سایر')], max_length=50, verbose_name='دسته\u200cبندی')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
],
options={
'verbose_name': 'تماس با ما',
'verbose_name_plural': 'تماس\u200cهای با ما',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['-created_at'], name='api_contact_created_d03dd7_idx'), models.Index(fields=['category'], name='api_contact_categor_76ad37_idx')],
},
),
migrations.CreateModel(
name='Composition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='نام')),
('description', models.TextField(verbose_name='توضیحات')),
('image', models.ImageField(blank=True, null=True, upload_to='compositions/', verbose_name='تصویر')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
],
options={
'verbose_name': 'ترکیب',
'verbose_name_plural': 'ترکیبات',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['-created_at'], name='api_composi_created_ac63fb_idx')],
},
),
migrations.CreateModel(
name='Campaign',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='نام')),
('description', models.TextField(verbose_name='توضیحات')),
('start_time', models.DateTimeField(verbose_name='زمان شروع')),
('end_time', models.DateTimeField(verbose_name='زمان پایان')),
('image', models.ImageField(blank=True, null=True, upload_to='campaigns/', verbose_name='تصویر')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
],
options={
'verbose_name': 'کمپین',
'verbose_name_plural': 'کمپین\u200cها',
'ordering': ['-start_time'],
'indexes': [models.Index(fields=['-start_time'], name='api_campaig_start_t_c79399_idx'), models.Index(fields=['end_time'], name='api_campaig_end_tim_d3f700_idx')],
},
),
]

View File

113
api/models.py Normal file
View File

@@ -0,0 +1,113 @@
from django.db import models
from django.core.validators import EmailValidator
from django.utils import timezone
class ContactUs(models.Model):
"""
Contact Us model for handling contact form submissions.
"""
CATEGORY_CHOICES = [
('همکاری', 'همکاری'),
('فروش', 'فروش'),
('پشتیبانی', 'پشتیبانی'),
('درخواست مشاور', 'درخواست مشاور'),
('سایر', 'سایر'),
]
name = models.CharField(max_length=200, verbose_name='نام و نام خانوادگی')
email_or_phone = models.CharField(
max_length=200,
verbose_name='ایمیل / شماره تماس',
help_text='Can be either email or phone number'
)
description = models.TextField(verbose_name='توضیحات')
category = models.CharField(
max_length=50,
choices=CATEGORY_CHOICES,
verbose_name='دسته‌بندی'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')
updated_at = models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')
class Meta:
verbose_name = 'تماس با ما'
verbose_name_plural = 'تماس‌های با ما'
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['category']),
]
def __str__(self):
return f"{self.name} - {self.category}"
class Composition(models.Model):
"""
Composition model for storing composition information.
"""
name = models.CharField(max_length=200, verbose_name='نام')
description = models.TextField(verbose_name='توضیحات')
image = models.ImageField(
upload_to='compositions/',
verbose_name='تصویر',
null=True,
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')
updated_at = models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')
class Meta:
verbose_name = 'ترکیب'
verbose_name_plural = 'ترکیبات'
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
]
def __str__(self):
return self.name
class Campaign(models.Model):
"""
Campaign model for storing campaign information.
"""
name = models.CharField(max_length=200, verbose_name='نام')
description = models.TextField(verbose_name='توضیحات')
start_time = models.DateTimeField(verbose_name='زمان شروع')
end_time = models.DateTimeField(verbose_name='زمان پایان')
image = models.ImageField(
upload_to='campaigns/',
verbose_name='تصویر',
null=True,
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')
updated_at = models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')
class Meta:
verbose_name = 'کمپین'
verbose_name_plural = 'کمپین‌ها'
ordering = ['-start_time']
indexes = [
models.Index(fields=['-start_time']),
models.Index(fields=['end_time']),
]
def __str__(self):
return self.name
def is_active(self):
"""Check if campaign is currently active."""
now = timezone.now()
return self.start_time <= now <= self.end_time
def is_upcoming(self):
"""Check if campaign is upcoming."""
return timezone.now() < self.start_time
def is_ended(self):
"""Check if campaign has ended."""
return timezone.now() > self.end_time

126
api/serializers.py Normal file
View File

@@ -0,0 +1,126 @@
from rest_framework import serializers
from .models import ContactUs, Composition, Campaign
class ContactUsSerializer(serializers.ModelSerializer):
"""
Serializer for ContactUs model.
"""
class Meta:
model = ContactUs
fields = ['id', 'name', 'email_or_phone', 'description', 'category',
'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
def validate_email_or_phone(self, value):
"""Validate that the field contains either email or phone."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Email or phone number is required.")
return value.strip()
def validate_name(self, value):
"""Validate name field."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Name is required.")
return value.strip()
def validate_description(self, value):
"""Validate description field."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Description is required.")
return value.strip()
class CompositionSerializer(serializers.ModelSerializer):
"""
Serializer for Composition model.
"""
image_url = serializers.SerializerMethodField()
class Meta:
model = Composition
fields = ['id', 'name', 'description', 'image', 'image_url',
'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at', 'image_url']
def get_image_url(self, obj):
"""Get the full URL of the image."""
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
def validate_name(self, value):
"""Validate name field."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Name is required.")
return value.strip()
def validate_description(self, value):
"""Validate description field."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Description is required.")
return value.strip()
class CampaignSerializer(serializers.ModelSerializer):
"""
Serializer for Campaign model.
"""
image_url = serializers.SerializerMethodField()
is_active = serializers.SerializerMethodField()
is_upcoming = serializers.SerializerMethodField()
is_ended = serializers.SerializerMethodField()
class Meta:
model = Campaign
fields = ['id', 'name', 'description', 'start_time', 'end_time',
'image', 'image_url', 'is_active', 'is_upcoming', 'is_ended',
'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at', 'image_url',
'is_active', 'is_upcoming', 'is_ended']
def get_image_url(self, obj):
"""Get the full URL of the image."""
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
def get_is_active(self, obj):
"""Get campaign active status."""
return obj.is_active()
def get_is_upcoming(self, obj):
"""Get campaign upcoming status."""
return obj.is_upcoming()
def get_is_ended(self, obj):
"""Get campaign ended status."""
return obj.is_ended()
def validate(self, data):
"""Validate that end_time is after start_time."""
if 'start_time' in data and 'end_time' in data:
if data['end_time'] <= data['start_time']:
raise serializers.ValidationError({
'end_time': 'End time must be after start time.'
})
return data
def validate_name(self, value):
"""Validate name field."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Name is required.")
return value.strip()
def validate_description(self, value):
"""Validate description field."""
if not value or len(value.strip()) == 0:
raise serializers.ValidationError("Description is required.")
return value.strip()

3
api/tests.py Normal file
View File

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

13
api/urls.py Normal file
View File

@@ -0,0 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ContactUsViewSet, CompositionViewSet, CampaignViewSet
router = DefaultRouter()
router.register(r'contact-us', ContactUsViewSet, basename='contact-us')
router.register(r'compositions', CompositionViewSet, basename='composition')
router.register(r'campaigns', CampaignViewSet, basename='campaign')
urlpatterns = [
path('', include(router.urls)),
]

114
api/views.py Normal file
View File

@@ -0,0 +1,114 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from django.db.models import Q
from django.utils import timezone
from .models import ContactUs, Composition, Campaign
from .serializers import ContactUsSerializer, CompositionSerializer, CampaignSerializer
class ContactUsViewSet(viewsets.ModelViewSet):
"""
ViewSet for ContactUs model.
Provides CRUD operations for contact form submissions.
"""
queryset = ContactUs.objects.all()
serializer_class = ContactUsSerializer
permission_classes = [AllowAny] # Allow anyone to submit contact forms
def get_permissions(self):
"""
Override to allow GET (list, retrieve) and POST (create) for anyone.
"""
if self.action in ['list', 'retrieve', 'create']:
return [AllowAny()]
return [IsAuthenticated()]
@action(detail=False, methods=['get'])
def by_category(self, request):
"""Get contacts filtered by category."""
category = request.query_params.get('category', None)
if category:
contacts = self.queryset.filter(category=category)
serializer = self.get_serializer(contacts, many=True)
return Response(serializer.data)
return Response(
{'error': 'Category parameter is required.'},
status=status.HTTP_400_BAD_REQUEST
)
class CompositionViewSet(viewsets.ModelViewSet):
"""
ViewSet for Composition model.
Provides CRUD operations for compositions.
"""
queryset = Composition.objects.all()
serializer_class = CompositionSerializer
permission_classes = [AllowAny] # Public read access
def get_permissions(self):
"""
Override to allow GET (list, retrieve) and POST (create) for anyone.
"""
if self.action in ['list', 'retrieve', 'create']:
return [AllowAny()]
return [IsAuthenticated()]
def get_serializer_context(self):
"""Add request to serializer context for image URL generation."""
context = super().get_serializer_context()
context['request'] = self.request
return context
class CampaignViewSet(viewsets.ModelViewSet):
"""
ViewSet for Campaign model.
Provides CRUD operations for campaigns.
"""
queryset = Campaign.objects.all()
serializer_class = CampaignSerializer
permission_classes = [AllowAny] # Public read access
def get_permissions(self):
"""
Override to allow GET (list, retrieve) and POST (create) for anyone.
"""
if self.action in ['list', 'retrieve', 'create', 'active', 'upcoming', 'ended']:
return [AllowAny()]
return [IsAuthenticated()]
def get_serializer_context(self):
"""Add request to serializer context for image URL generation."""
context = super().get_serializer_context()
context['request'] = self.request
return context
@action(detail=False, methods=['get'])
def active(self, request):
"""Get all currently active campaigns."""
now = timezone.now()
active_campaigns = self.queryset.filter(
start_time__lte=now,
end_time__gte=now
)
serializer = self.get_serializer(active_campaigns, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def upcoming(self, request):
"""Get all upcoming campaigns."""
now = timezone.now()
upcoming_campaigns = self.queryset.filter(start_time__gt=now)
serializer = self.get_serializer(upcoming_campaigns, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def ended(self, request):
"""Get all ended campaigns."""
now = timezone.now()
ended_campaigns = self.queryset.filter(end_time__lt=now)
serializer = self.get_serializer(ended_campaigns, many=True)
return Response(serializer.data)

22
manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zonco_backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
Django==4.2.7
djangorestframework==3.14.0
psycopg2-binary==2.9.11
django-cors-headers==4.3.1
Pillow==12.0.0

View File

16
zonco_backend/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for zonco_backend 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/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zonco_backend.settings')
application = get_asgi_application()

186
zonco_backend/settings.py Normal file
View File

@@ -0,0 +1,186 @@
"""
Django settings for zonco_backend project.
Generated by 'django-admin startproject' using Django 4.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
# 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/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-ro-sqgr@$*(qao)@d&ezk*z9&%+vbeurgi$b+6y650j*$1b+n5'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'rest_framework',
'corsheaders',
# Local apps
'api',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'zonco_backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'zonco_backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'Zoneco_ORG',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Media files
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# REST Framework configuration
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
'rest_framework.parsers.FormParser',
],
}
# CORS configuration
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173", # Vite default port
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]

30
zonco_backend/urls.py Normal file
View File

@@ -0,0 +1,30 @@
"""
URL configuration for zonco_backend project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
zonco_backend/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for zonco_backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zonco_backend.settings')
application = get_wsgi_application()