diff --git a/server/.env.example b/server/.env.example index 07a571f..7dba888 100644 --- a/server/.env.example +++ b/server/.env.example @@ -15,6 +15,14 @@ DB_PORT=5432 SITE_URL='http://localhost:8000' +# OpenAI +OPENAI_API_KEY=xxxxxx + +# Cloudinary +CLOUDINARY_CLOUD_NAME=xxxx +CLOUDINARY_API_KEY=123456789 +CLOUDINARY_API_SECRET=xxxxxxxxxxxxx + # Django smtp EMAIL_HOST = 'smtp.gmail.com' # Example using Gmail EMAIL_HOST_USER = 'enter your email' diff --git a/server/apps/research/migrations/0017_alter_article_thumb.py b/server/apps/research/migrations/0017_alter_article_thumb.py deleted file mode 100644 index 30f4c6f..0000000 --- a/server/apps/research/migrations/0017_alter_article_thumb.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.8 on 2024-12-14 11:13 - -import apps.research.models.article -import cloudinary.models -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('research', '0016_article_related_articles'), - ] - - operations = [ - migrations.AlterField( - model_name='article', - name='thumb', - field=cloudinary.models.CloudinaryField(blank=True, default=apps.research.models.article.get_default_thumb, max_length=255, verbose_name='image'), - ), - ] diff --git a/server/apps/research/migrations/0018_alter_article_thumb.py b/server/apps/research/migrations/0018_alter_article_thumb.py new file mode 100644 index 0000000..58288ab --- /dev/null +++ b/server/apps/research/migrations/0018_alter_article_thumb.py @@ -0,0 +1,59 @@ +import apps.research.models.article +import cloudinary.models +import cloudinary.uploader +from django.db import migrations +from django.core.files.storage import default_storage +import os + + +def transfer_to_cloudinary(apps, schema_editor): + Article = apps.get_model("research", "Article") + + for instance in Article.objects.all(): + if instance.thumb: + try: + if hasattr(instance.thumb, "public_id"): + continue + + if hasattr(instance.thumb, "url"): + file_path = instance.thumb.path + + if default_storage.exists(file_path): + with open(file_path, "rb") as file: + upload_result = cloudinary.uploader.upload( + file, folder="coverImage", resource_type="image" + ) + + instance.thumb = upload_result["public_id"] + instance.save() + + # delete the local file + # default_storage.delete(file_path) + + except Exception as e: + print(f"Info for Article {instance.id}: {str(e)}") + + +def reverse_transfer(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("research", "0017_article_gpt_summary_alter_article_summary"), + ] + + operations = [ + migrations.AlterField( + model_name="article", + name="thumb", + field=cloudinary.models.CloudinaryField( + blank=True, + default=apps.research.models.article.get_default_thumb, + max_length=255, + verbose_name="image", + ), + ), + migrations.RunPython(transfer_to_cloudinary, reverse_transfer), + ] diff --git a/server/apps/research/models/article.py b/server/apps/research/models/article.py index 6b1c3b3..6d6e83e 100644 --- a/server/apps/research/models/article.py +++ b/server/apps/research/models/article.py @@ -15,7 +15,7 @@ from cloudinary.models import CloudinaryField def get_default_thumb(): - return f"{settings.MEDIA_URL}images/2077-Collective.png" + return "v1734517759/v4_article_cover_slashing_hhf6tz" class Article(BaseModel): """Model for articles.""" @@ -121,7 +121,7 @@ def save(self, *args, **kwargs): self.slug = self.generate_unique_slug() """Override the save method to track slug changes.""" - if self.pk: # If this is an existing article + if self.pk: try: old_instance = Article.objects.get(pk=self.pk) # Generate new slug first @@ -144,8 +144,17 @@ def save(self, *args, **kwargs): if self.scheduled_publish_time and self.status == 'draft' and timezone.now() >= self.scheduled_publish_time: self.status = 'ready' - - super().save(*args, **kwargs) + + if self.thumb and not hasattr(self.thumb, 'public_id'): + super().save(*args, **kwargs) + elif self.thumb and hasattr(self.thumb, 'public_id'): + try: + if not self.thumb.public_id: + raise ValidationError("Failed to upload image to Cloudinary") + except Exception as e: + raise ValidationError(f"Image upload failed: {str(e)}") + else: + super().save(*args, **kwargs) def generate_unique_slug(self): """Generate a unique slug for the article.""" diff --git a/server/apps/research/serializers/article_serializer.py b/server/apps/research/serializers/article_serializer.py index f824563..336a781 100644 --- a/server/apps/research/serializers/article_serializer.py +++ b/server/apps/research/serializers/article_serializer.py @@ -1,7 +1,9 @@ +import logging from rest_framework import serializers from ..models import Article, Author, Category from .author_serializer import AuthorSerializer from .category_serializer import CategorySerializer +from django.conf import settings class RelatedArticleSerializer(serializers.ModelSerializer): @@ -69,7 +71,7 @@ class ArticleSerializer(serializers.ModelSerializer): def get_thumb(self, obj): if obj.thumb: - return f"https://res.cloudinary.com/dc2iz5j1c/{obj.thumb}" + return f"{settings.CLOUDINARY_DOMAIN}/{obj.thumb}" return None def get_related_articles(self, obj): @@ -168,7 +170,8 @@ def create(self, validated_data: dict) -> Article: return article except Exception as e: - raise serializers.ValidationError(f"Error creating article: {str(e)}") + logging.error(f"Error creating article: {str(e)}") + raise serializers.ValidationError("An error occurred while creating the article.") def update(self, instance: Article, validated_data: dict) -> Article: """Update an existing article instance.""" @@ -188,4 +191,5 @@ def update(self, instance: Article, validated_data: dict) -> Article: return instance except Exception as e: - raise serializers.ValidationError(f"Error updating article: {str(e)}") \ No newline at end of file + logging.error(f"Error updating article: {str(e)}") + raise serializers.ValidationError("An error occurred while updating the article.") \ No newline at end of file diff --git a/server/apps/research/urls.py b/server/apps/research/urls.py index 7aebdc7..37edcbf 100644 --- a/server/apps/research/urls.py +++ b/server/apps/research/urls.py @@ -5,7 +5,6 @@ from rest_framework.routers import DefaultRouter from .views import ArticleViewSet from .redirects_urls import urlpatterns as redirects_urlpatterns -from django.urls import path from .views import tinymce_upload_image router = DefaultRouter() diff --git a/server/apps/research/views.py b/server/apps/research/views.py index 6fa7d9d..5e2559a 100644 --- a/server/apps/research/views.py +++ b/server/apps/research/views.py @@ -130,14 +130,17 @@ def tinymce_upload_image(request): if request.method == "POST" and request.FILES: try: file = request.FILES['file'] - # Upload to a specific folder in Cloudinary upload_data = cloudinary.uploader.upload( file, - folder='article_content' # This will create an article_content folder in Cloudinary + folder='article_content' ) return JsonResponse({ 'location': upload_data['secure_url'] }) except Exception as e: - return JsonResponse({'error': str(e)}) - return JsonResponse({'error': 'Invalid request'}) \ No newline at end of file + logger.error(f"Error uploading image: {str(e)}") + return JsonResponse( + {'error': 'An error occurred while uploading the image'}, + status=500 + ) + return JsonResponse({'error': 'Invalid request'}, status=400) \ No newline at end of file diff --git a/server/core/config/base.py b/server/core/config/base.py index d043e3b..f1bb497 100644 --- a/server/core/config/base.py +++ b/server/core/config/base.py @@ -17,7 +17,6 @@ # cloudinary import cloudinary import cloudinary.uploader -import cloudinary.api # third party imports from .celery_config import (CELERY_BROKER_URL, CELERY_RESULT_BACKEND, CELERY_ACCEPT_CONTENT, CELERY_TASK_SERIALIZER, CELERY_RESULT_SERIALIZER, CELERY_TIMEZONE) @@ -25,6 +24,7 @@ load_dotenv() from decouple import config +from .cloudinary import CLOUDINARY_DOMAIN, CLOUDINARY_STORAGE # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -180,13 +180,6 @@ os.path.join(BASE_DIR, 'static'), ] -# cloudinary storage -CLOUDINARY_STORAGE = { - 'CLOUD_NAME': config('CLOUDINARY_CLOUD_NAME'), - 'API_KEY': config('CLOUDINARY_API_KEY'), - 'API_SECRET': config('CLOUDINARY_API_SECRET') -} - STORAGES = { "default": { "BACKEND": "cloudinary_storage.storage.MediaCloudinaryStorage", @@ -196,19 +189,6 @@ }, } -TINYMCE_DEFAULT_CONFIG = { - 'images_upload_url': '/tinymce/upload/', - 'images_upload_base_path': '', - 'images_upload_credentials': True, - 'file_picker_types': 'image', - 'automatic_uploads': True, - 'images_file_types': 'jpg,svg,webp,png', - 'content_css': 'default', - 'plugins': 'image autolink lists media table', - 'toolbar1': 'formatselect | bold italic underline | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent | table | link image media | removeformat', - 'width': '100%', - 'height': 400 -} # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/server/core/config/cloudinary.py b/server/core/config/cloudinary.py new file mode 100644 index 0000000..e3ba7f6 --- /dev/null +++ b/server/core/config/cloudinary.py @@ -0,0 +1,9 @@ +from decouple import config + +CLOUDINARY_DOMAIN = "https://res.cloudinary.com/dc2iz5j1c" + +CLOUDINARY_STORAGE = { + 'CLOUD_NAME': config('CLOUDINARY_CLOUD_NAME'), + 'API_KEY': config('CLOUDINARY_API_KEY'), + 'API_SECRET': config('CLOUDINARY_API_SECRET') +} \ No newline at end of file