update README file and create requirements.txt

This commit is contained in:
Ahmed Nagi 2025-01-25 21:15:23 +02:00
parent 06513fa78b
commit ebecba4fb1
19 changed files with 1002 additions and 0 deletions

278
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,278 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
staticfiles/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# Environments
.venv
venv/
ENV/
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for devcontainer
.devcontainer/bash_history
### Windows template
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### SublimeText template
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### Vim template
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Redis dump file
dump.rdb
### Project template
# e_commerce/media/
.pytest_cache/
.ipython/

View file

View file

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

View file

@ -0,0 +1,171 @@
"""
Django settings for Inspire_Ink project.
Generated by 'django-admin startproject' using Django 5.1.5.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/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/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-42klz+7n0(j7@13+0%=-5nd@i)^3g-t7(br-3drg0u%xg9%a&r'
# 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',
'rest_framework',
'rest_framework_simplejwt',
'djoser',
"corsheaders",
"app",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'Inspire_Ink.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 = 'Inspire_Ink.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
AUTH_USER_MODEL = 'app.User'
# Password validation
# https://docs.djangoproject.com/en/5.1/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/5.1/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/5.1/howto/static-files/
STATIC_URL = 'static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / "media"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
# 'AUTH_HEADER_TYPES': ('JWT',),
}
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_METHODS = (
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
)
CORS_ALLOW_HEADERS = (
"accept",
"authorization",
"content-type",
"user-agent",
"x-csrftoken",
"x-requested-with",
)

View file

@ -0,0 +1,33 @@
"""
URL configuration for Inspire_Ink project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/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, re_path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
re_path(r'^auth/', include('djoser.urls')),
re_path(r'^auth/', include('djoser.urls.jwt')),
path('app/', include('app.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -0,0 +1,16 @@
"""
WSGI config for Inspire_Ink 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/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Inspire_Ink.settings')
application = get_wsgi_application()

0
backend/app/README.md Normal file
View file

0
backend/app/__init__.py Normal file
View file

43
backend/app/admin.py Normal file
View file

@ -0,0 +1,43 @@
from django.contrib import admin
from .models import User, Category, Article, Comment, Like, Notification
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ('username', 'email', 'is_staff', 'is_active', 'date_joined')
search_fields = ('username', 'email')
list_filter = ('is_staff', 'is_active')
ordering = ('date_joined',)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'created_at')
search_fields = ('name',)
ordering = ('created_at',)
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'category', 'created_at', 'updated_at')
search_fields = ('title', 'author__username')
list_filter = ('category',)
ordering = ('created_at',)
autocomplete_fields = ('author', 'category')
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('content', 'author', 'article', 'created_at', 'id')
search_fields = ('content', 'author__username', 'article__title')
ordering = ('created_at',)
@admin.register(Like)
class LikeAdmin(admin.ModelAdmin):
list_display = ('user', 'article', 'created_at')
search_fields = ('user__username', 'article__title')
ordering = ('created_at',)
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
list_display = ('recipient', 'content', 'created_at', 'is_read')
search_fields = ('recipient__username', 'content')
list_filter = ('is_read',)
ordering = ('created_at',)

6
backend/app/apps.py Normal file
View file

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

104
backend/app/models.py Normal file
View file

@ -0,0 +1,104 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
import uuid
class User(AbstractUser):
"""
Represents a user of the blog application. Extends the default Django AbstractUser to include additional fields.
"""
bio = models.TextField(null=True, blank=True, help_text="A short biography of the user.")
profile_image = models.ImageField(upload_to="profile_images/", null=True, blank=True, help_text="Profile image of the user.")
followers = models.ManyToManyField("self", symmetrical=False, related_name="following", blank=True, help_text="Users who follow this user.")
def __str__(self):
"""Returns the username of the user."""
return self.username
class Article(models.Model):
"""
Represents an article written by a user.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles", help_text="The author of the article.")
title = models.CharField(max_length=50, help_text="The title of the article.")
introduction = models.CharField(max_length=300, help_text="The introduction of the article.")
content = models.TextField(null=True, blank=True, help_text="The content of the article.")
thumbnail = models.ImageField(upload_to="article_thumbnails/", null=True, blank=True, help_text="An optional thumbnail image for the article.")
created_at = models.DateTimeField(auto_now_add=True, help_text="The timestamp when the article was created.")
updated_at = models.DateTimeField(auto_now=True, help_text="The timestamp when the article was last updated.")
category = models.ForeignKey("Category", on_delete=models.SET_NULL, null=True, blank=True, related_name="articles", help_text="The category of the article.")
def __str__(self):
"""Returns the title of the article."""
return self.title
class Category(models.Model):
"""
Represents a category to organize articles.
"""
name = models.CharField(max_length=100, unique=True, help_text="The unique name of the category.")
description = models.TextField(null=True, blank=True, help_text="A short description of the category.")
created_at = models.DateTimeField(auto_now_add=True, help_text="The timestamp when the category was created.")
def __str__(self):
"""Returns the name of the category."""
return self.name
class Comment(models.Model):
"""
Represents a comment on an article.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments", help_text="The article this comment belongs to.")
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments", help_text="The author of the comment.")
content = models.TextField(help_text="The content of the comment.")
created_at = models.DateTimeField(auto_now_add=True, help_text="The timestamp when the comment was created.")
def __str__(self):
"""Returns a short description of the comment."""
return f"Comment by {self.author.username} on {self.article.title}"
class Like(models.Model):
"""
Represents a like for an article or a comment.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_likes", help_text="The user who liked the article or comment.")
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="article_likes", null=True, blank=True, help_text="The article that was liked, if applicable.")
created_at = models.DateTimeField(auto_now_add=True, help_text="The timestamp when the like was created.")
def __str__(self):
"""Returns a short description of the like."""
if self.article:
return f"{self.user.username} liked {self.article.title}"
elif self.comment:
return f"{self.user.username} liked a comment"
class Notification(models.Model):
"""
Represents a notification for a user.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications", help_text="The user who will receive the notification.")
content = models.CharField(max_length=255, help_text="The content of the notification.")
created_at = models.DateTimeField(auto_now_add=True, help_text="The timestamp when the notification was created.")
is_read = models.BooleanField(default=False, help_text="Indicates whether the notification has been read.")
def __str__(self):
"""Returns a short description of the notification."""
return f"Notification for {self.recipient.username}"
class View(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="views")
id_address = models.GenericIPAddressField(null=True, blank=True)
session_id = models.CharField(max_length=255, null=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"View on {self.article.title}"

View file

@ -0,0 +1,83 @@
from rest_framework import serializers
from .models import *
import os
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = "__all__"
read_only_fields = ["article", 'author', 'created_at']
class ArticleSerializer(serializers.ModelSerializer):
author = serializers.CharField(source='author.username', read_only=True)
image = serializers.SerializerMethodField()
comments = CommentSerializer(many=True, read_only=True)
likes_count = serializers.IntegerField(read_only=True)
class Meta:
model = Article
fields = "__all__"
read_only_fields = ['created_at', 'updated_at']
def get_image(self, obj):
request = self.context.get('request')
if obj.thumbnail:
return request.build_absolute_uri(obj.thumbnail.url)
return None
def validate_thumbnail(self, value):
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
ext = os.path.splitext(value.name)[1].lower()
if ext not in valid_extensions:
raise serializers.ValidationError("JPG, JPEG, PNG, GIF, WebP.")
return value
def create(self, validated_data):
"""
Override the create method to use update_or_create.
"""
title = validated_data.get('title')
defaults = {
"introduction": validated_data.get('introduction'),
"content": validated_data.get('content'),
"author": self.context['request'].user,
"category": validated_data.get('category'),
"thumbnail": validated_data.get('thumbnail'),
}
article, created = Article.objects.update_or_create(author=self.context['request'].user, title=title, defaults=defaults)
return article, created
class ArticleListSerializer(serializers.ModelSerializer):
author = serializers.CharField(source='author.username', read_only=True)
likes_count = serializers.IntegerField(read_only=True)
image = serializers.SerializerMethodField()
class Meta:
model = Article
fields = ["id", "author", "title", "image", "likes_count", 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
def get_image(self, obj):
request = self.context.get('request')
if obj.thumbnail:
return request.build_absolute_uri(obj.thumbnail.url)
return None
class UserSerializer(serializers.ModelSerializer):
"""
Serializer for User model that includes the user's articles.
"""
articles = ArticleSerializer(many=True, read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'email', 'bio', 'profile_image', 'followers', 'articles']

View file

View file

@ -0,0 +1,113 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from app.models import Category, Tag, Article, Comment, Like, Notification
User = get_user_model()
class BlogModelsTest(TestCase):
def setUp(self):
"""
Set up test data for all the models.
"""
self.user1 = User.objects.create_user(username="testuser1", password="password123", bio="Bio for user1")
self.user2 = User.objects.create_user(username="testuser2", password="password123")
self.category = Category.objects.create(name="Technology", description="All about tech.")
self.tag1 = Tag.objects.create(name="Python")
self.tag2 = Tag.objects.create(name="Django")
self.article = Article.objects.create(
author=self.user1,
title="Test Article",
content="This is a test article.",
category=self.category,
)
self.article.tags.add(self.tag1, self.tag2)
# Create comment
self.comment = Comment.objects.create(
article=self.article,
author=self.user2,
content="This is a test comment."
)
# Create like
self.like = Like.objects.create(user=self.user2, article=self.article)
# Create notification
self.notification = Notification.objects.create(
recipient=self.user1,
content="You have a new like on your article."
)
def test_user_creation(self):
"""
Test the User model and its additional fields.
"""
self.assertEqual(self.user1.bio, "Bio for user1")
self.assertEqual(self.user1.followers.count(), 0)
def test_category_creation(self):
"""
Test the Category model.
"""
self.assertEqual(self.category.name, "Technology")
self.assertEqual(self.category.description, "All about tech.")
def test_tag_creation(self):
"""
Test the Tag model.
"""
self.assertEqual(Tag.objects.count(), 2)
self.assertIn(self.tag1, self.article.tags.all())
def test_article_creation(self):
"""
Test the Article model.
"""
self.assertEqual(self.article.title, "Test Article")
self.assertEqual(self.article.author, self.user1)
self.assertEqual(self.article.category, self.category)
self.assertEqual(self.article.tags.count(), 2)
def test_comment_creation(self):
"""
Test the Comment model.
"""
self.assertEqual(self.comment.article, self.article)
self.assertEqual(self.comment.author, self.user2)
self.assertEqual(self.comment.content, "This is a test comment.")
def test_like_creation(self):
"""
Test the Like model.
"""
self.assertEqual(self.like.user, self.user2)
self.assertEqual(self.like.article, self.article)
self.assertIsNone(self.like.comment)
def test_notification_creation(self):
"""
Test the Notification model.
"""
self.assertEqual(self.notification.recipient, self.user1)
self.assertEqual(self.notification.content, "You have a new like on your article.")
self.assertFalse(self.notification.is_read)
def test_article_view_increment(self):
"""
Test manually incrementing the views for an article.
"""
self.article.views += 1
self.article.save()
self.assertEqual(self.article.views, 1)
def test_followers(self):
"""
Test the followers functionality.
"""
self.user1.followers.add(self.user2)
self.assertEqual(self.user1.followers.count(), 1)
self.assertIn(self.user2, self.user1.followers.all())

13
backend/app/urls.py Normal file
View file

@ -0,0 +1,13 @@
from django.urls import path
from . import views
urlpatterns = [
path('article/', views.ArticleView.as_view()),
path('article/<uuid:pk>/', views.ArticleView.as_view()), # for Retrieve & Delete object
path('comment/<uuid:pk>/', views.CommentView.as_view()),
path('like/<uuid:pk>/', views.LikeView.as_view()),
path('user/', views.UserDetailView.as_view()),
]

104
backend/app/views.py Normal file
View file

@ -0,0 +1,104 @@
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from django.db.models import Count
from .models import *
from .serializers import *
from rest_framework.permissions import IsAuthenticated
from django.utils.decorators import method_decorator
class ArticleView(APIView):
def get(self, request, pk=None):
if pk:
article = get_object_or_404(
Article.objects
.select_related('author', 'category')
.prefetch_related('comments')
.annotate(likes_count=Count('article_likes')),
pk=pk
)
serializer = ArticleSerializer(article, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
articles = (
Article.objects
.select_related('author', 'category')
.annotate(likes_count=Count('article_likes'))
)
serializer = ArticleListSerializer(articles, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request):
serializer = ArticleSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
article, created = serializer.save()
if created:
return Response(
{"detail": "Article created successfully", "article_id": article.id},
status=status.HTTP_201_CREATED,
)
else:
return Response(
{"detail": "Article updated successfully", "article_id": article.id},
status=status.HTTP_200_OK,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
if not pk:
return Response(status=status.HTTP_404_NOT_FOUND)
article = get_object_or_404(Article.objects.filter(author=request.user).select_related("author").only("id","author"), pk=pk)
article.delete()
return Response({"detail": "Article deleted successfully."}, status=status.HTTP_204_NO_CONTENT)
class CommentView(APIView):
def post(self, request, pk): # pk for article
article = get_object_or_404(Article.objects.only('id'), pk=pk)
if Comment.objects.filter(article=article, author=request.user).exists():
return Response({"detail": "You have already commented on this article."}, status=status.HTTP_400_BAD_REQUEST)
serializer = CommentSerializer(data= request.data)
if serializer.is_valid():
serializer.save(article=article, author=request.user)
return Response({"detail": "Comment created successfully"}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk): # pk for comment
comment = get_object_or_404(Comment.objects.filter(author=request.user).select_related("author").only('id', "author"), pk=pk)
comment.delete()
return Response({"detail": "Comment deleted successfully."}, status=status.HTTP_204_NO_CONTENT)
class LikeView(APIView):
def post(self, request, pk): # pk for article
article = get_object_or_404(Article.objects.only('id'), pk=pk)
# Try to delete the like if it exists, otherwise create it
like_deleted, _ = Like.objects.filter(article=article, user=request.user).delete()
if like_deleted:
return Response({"detail": "Like deleted successfully."}, status=status.HTTP_204_NO_CONTENT)
Like.objects.create(article=article, user=request.user)
return Response({"detail": "Like created successfully"}, status=status.HTTP_201_CREATED)
class UserDetailView(APIView):
"""
API view to retrieve a user's details along with their articles.
"""
def get(self, request):
user = User.objects.get(id=request.user.id)
serializer = UserSerializer(user, context={'request': request})
return Response(serializer.data)

BIN
backend/db.sqlite3 Normal file

Binary file not shown.

22
backend/manage.py Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Inspire_Ink.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()

BIN
backend/requirements.txt Normal file

Binary file not shown.