update README file and create requirements.txt

This commit is contained in:
Ahmed Nagi 2025-01-25 21:16:28 +02:00
parent ebecba4fb1
commit 0c6096f3d2
21 changed files with 18 additions and 1018 deletions

278
Inspire_Ink/.gitignore vendored
View file

@ -1,278 +0,0 @@
### 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

@ -1,16 +0,0 @@
"""
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

@ -1,171 +0,0 @@
"""
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

@ -1,34 +0,0 @@
"""
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),
path('lo/', include('rest_framework.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

@ -1,16 +0,0 @@
"""
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()

View file

@ -1,43 +0,0 @@
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',)

View file

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

View file

@ -1,104 +0,0 @@
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

@ -1,83 +0,0 @@
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

@ -1,113 +0,0 @@
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())

View file

@ -1,13 +0,0 @@
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()),
]

View file

@ -1,104 +0,0 @@
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)

Binary file not shown.

View file

@ -1,22 +0,0 @@
#!/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()

View file

@ -1,4 +1,5 @@
# Inspire & Ink Blog
# Inspire & Ink
![Image](./screenshot/image.jpg)
## Project Description
@ -14,7 +15,11 @@
### 2. Content Creation
- **Rich text editor (WYSIWYG)** for writing articles with HTML content storage.
- Create, update, delete, and view blog posts.
<img src="./screenshot/WYSIWYG.png" alt="Alt text" width="600"/>
- Create, update, delete, and view article posts.
### 3. Frontend Design
- Built with **Vue.js**, providing a responsive and dynamic user interface.
@ -22,13 +27,9 @@
### 4. Backend Functionality
- **RESTful API** built using Django Rest Framework (DRF).
- Well-structured endpoints for managing blog posts and user profiles.
- Well-structured endpoints for managing article posts and user profiles.
### 5. Search and Filtering
- **Full-text search** for blog posts.
- Filtering and sorting options for easier navigation of content.
### 6. Secure Content Management
### 5. Secure Content Management
- **Role-based permissions** to manage users and restrict access.
- Secure storage of content with token-based authentication.
@ -58,7 +59,9 @@
1. Clone the repository:
```bash
git clone https://github.com/Ahmed-Nagi1/Inspire-Ink
cd inspire-and-ink/backend
cd ./backend
```
2. Create and activate a virtual environment:
@ -71,13 +74,15 @@ source env/bin/activate
3. Install dependencies:
pip install -r requirements.txt
`pip install -r requirements.txt`
4. Apply database migrations:
```
python manage.py makemigrations
python manage.py migrate
```
5. Run the development server:
@ -90,7 +95,7 @@ Frontend Setup:
1. Navigate to the frontend directory:
cd ./frontend
`cd ./frontend`
2. Follow the instructions in the README.md file for further setup.
@ -103,12 +108,10 @@ Usage
1. Access the frontend at http://localhost:3000 (or the port specified by Vue.js).
2. API endpoints are available at http://127.0.0.1:8000/api/.
2. API endpoints are available at http://127.0.0.1:8000/.
3. Register a new user or log in to access content creation features.
4. Use the rich text editor to create and format blog posts.
4. Use the rich text editor to create and format article posts.

BIN
screenshot/WYSIWYG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
screenshot/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB