Initial commit: Django backend with Campaign, ContactUs, and Composition APIs
This commit is contained in:
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal 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
135
README.md
Normal 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
|
||||||
|
|
||||||
352
Zoneco_ORG_API.postman_collection.json
Normal file
352
Zoneco_ORG_API.postman_collection.json
Normal 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
0
api/__init__.py
Normal file
72
api/admin.py
Normal file
72
api/admin.py
Normal 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
6
api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'api'
|
||||||
0
api/management/__init__.py
Normal file
0
api/management/__init__.py
Normal file
0
api/management/commands/__init__.py
Normal file
0
api/management/commands/__init__.py
Normal file
125
api/management/commands/seed_data.py
Normal file
125
api/management/commands/seed_data.py
Normal 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')
|
||||||
68
api/migrations/0001_initial.py
Normal file
68
api/migrations/0001_initial.py
Normal 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')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
api/migrations/__init__.py
Normal file
0
api/migrations/__init__.py
Normal file
113
api/models.py
Normal file
113
api/models.py
Normal 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
126
api/serializers.py
Normal 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
3
api/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
13
api/urls.py
Normal file
13
api/urls.py
Normal 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
114
api/views.py
Normal 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
22
manage.py
Normal 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
6
requirements.txt
Normal 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
|
||||||
|
|
||||||
0
zonco_backend/__init__.py
Normal file
0
zonco_backend/__init__.py
Normal file
16
zonco_backend/asgi.py
Normal file
16
zonco_backend/asgi.py
Normal 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
186
zonco_backend/settings.py
Normal 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
30
zonco_backend/urls.py
Normal 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
16
zonco_backend/wsgi.py
Normal 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()
|
||||||
Reference in New Issue
Block a user