commit 3e100bae99bf1ac002142c2759e4e324f3e31126 Author: Shayan Azadi Date: Wed Dec 17 15:58:07 2025 +0330 Initial commit: Django backend with Campaign, ContactUs, and Composition APIs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..265a334 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b48f226 --- /dev/null +++ b/README.md @@ -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 + diff --git a/Zoneco_ORG_API.postman_collection.json b/Zoneco_ORG_API.postman_collection.json new file mode 100644 index 0000000..e1e4031 --- /dev/null +++ b/Zoneco_ORG_API.postman_collection.json @@ -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" + } + ] +} + diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..81612e8 --- /dev/null +++ b/api/admin.py @@ -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 = 'فعال' diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/api/management/__init__.py b/api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/management/commands/__init__.py b/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/management/commands/seed_data.py b/api/management/commands/seed_data.py new file mode 100644 index 0000000..77d7cb8 --- /dev/null +++ b/api/management/commands/seed_data.py @@ -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') diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..25b3160 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -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')], + }, + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..14e5d4d --- /dev/null +++ b/api/models.py @@ -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 diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..8b5f724 --- /dev/null +++ b/api/serializers.py @@ -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() + diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..8a44b04 --- /dev/null +++ b/api/urls.py @@ -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)), +] + diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..2dd0034 --- /dev/null +++ b/api/views.py @@ -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) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..9ca543b --- /dev/null +++ b/manage.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..459b893 --- /dev/null +++ b/requirements.txt @@ -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 + diff --git a/zonco_backend/__init__.py b/zonco_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zonco_backend/asgi.py b/zonco_backend/asgi.py new file mode 100644 index 0000000..c6b1815 --- /dev/null +++ b/zonco_backend/asgi.py @@ -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() diff --git a/zonco_backend/settings.py b/zonco_backend/settings.py new file mode 100644 index 0000000..449ffaf --- /dev/null +++ b/zonco_backend/settings.py @@ -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', +] diff --git a/zonco_backend/urls.py b/zonco_backend/urls.py new file mode 100644 index 0000000..bb347cd --- /dev/null +++ b/zonco_backend/urls.py @@ -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) diff --git a/zonco_backend/wsgi.py b/zonco_backend/wsgi.py new file mode 100644 index 0000000..b8d2323 --- /dev/null +++ b/zonco_backend/wsgi.py @@ -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()