diff --git a/Inspire_Ink/.gitignore b/Inspire_Ink/.gitignore deleted file mode 100644 index af7249d..0000000 --- a/Inspire_Ink/.gitignore +++ /dev/null @@ -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/ \ No newline at end of file diff --git a/Inspire_Ink/Inspire_Ink/__init__.py b/Inspire_Ink/Inspire_Ink/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Inspire_Ink/Inspire_Ink/asgi.py b/Inspire_Ink/Inspire_Ink/asgi.py deleted file mode 100644 index e660820..0000000 --- a/Inspire_Ink/Inspire_Ink/asgi.py +++ /dev/null @@ -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() diff --git a/Inspire_Ink/Inspire_Ink/settings.py b/Inspire_Ink/Inspire_Ink/settings.py deleted file mode 100644 index d46fc28..0000000 --- a/Inspire_Ink/Inspire_Ink/settings.py +++ /dev/null @@ -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", -) \ No newline at end of file diff --git a/Inspire_Ink/Inspire_Ink/urls.py b/Inspire_Ink/Inspire_Ink/urls.py deleted file mode 100644 index 7e78cda..0000000 --- a/Inspire_Ink/Inspire_Ink/urls.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/Inspire_Ink/Inspire_Ink/wsgi.py b/Inspire_Ink/Inspire_Ink/wsgi.py deleted file mode 100644 index c3ad5ef..0000000 --- a/Inspire_Ink/Inspire_Ink/wsgi.py +++ /dev/null @@ -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() diff --git a/Inspire_Ink/app/README.md b/Inspire_Ink/app/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/Inspire_Ink/app/__init__.py b/Inspire_Ink/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Inspire_Ink/app/admin.py b/Inspire_Ink/app/admin.py deleted file mode 100644 index cd3c249..0000000 --- a/Inspire_Ink/app/admin.py +++ /dev/null @@ -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',) diff --git a/Inspire_Ink/app/apps.py b/Inspire_Ink/app/apps.py deleted file mode 100644 index ed327d2..0000000 --- a/Inspire_Ink/app/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class AppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'app' diff --git a/Inspire_Ink/app/models.py b/Inspire_Ink/app/models.py deleted file mode 100644 index b992ce5..0000000 --- a/Inspire_Ink/app/models.py +++ /dev/null @@ -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}" \ No newline at end of file diff --git a/Inspire_Ink/app/serializers.py b/Inspire_Ink/app/serializers.py deleted file mode 100644 index 9d39bfb..0000000 --- a/Inspire_Ink/app/serializers.py +++ /dev/null @@ -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'] \ No newline at end of file diff --git a/Inspire_Ink/app/tests/__init__.py b/Inspire_Ink/app/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Inspire_Ink/app/tests/test_models.py b/Inspire_Ink/app/tests/test_models.py deleted file mode 100644 index d1a3076..0000000 --- a/Inspire_Ink/app/tests/test_models.py +++ /dev/null @@ -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()) diff --git a/Inspire_Ink/app/urls.py b/Inspire_Ink/app/urls.py deleted file mode 100644 index 023f34b..0000000 --- a/Inspire_Ink/app/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('article/', views.ArticleView.as_view()), - path('article//', views.ArticleView.as_view()), # for Retrieve & Delete object - - path('comment//', views.CommentView.as_view()), - - path('like//', views.LikeView.as_view()), - - path('user/', views.UserDetailView.as_view()), -] diff --git a/Inspire_Ink/app/views.py b/Inspire_Ink/app/views.py deleted file mode 100644 index dd8e065..0000000 --- a/Inspire_Ink/app/views.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/Inspire_Ink/db.sqlite3 b/Inspire_Ink/db.sqlite3 deleted file mode 100644 index 4666aff..0000000 Binary files a/Inspire_Ink/db.sqlite3 and /dev/null differ diff --git a/Inspire_Ink/manage.py b/Inspire_Ink/manage.py deleted file mode 100644 index d47dbf9..0000000 --- a/Inspire_Ink/manage.py +++ /dev/null @@ -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() diff --git a/README.md b/README.md index 24ffb8e..ec3832b 100644 --- a/README.md +++ b/README.md @@ -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. + +Alt text + + +- 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. \ No newline at end of file diff --git a/screenshot/WYSIWYG.png b/screenshot/WYSIWYG.png new file mode 100644 index 0000000..6d48870 Binary files /dev/null and b/screenshot/WYSIWYG.png differ diff --git a/screenshot/image.jpg b/screenshot/image.jpg new file mode 100644 index 0000000..038cb82 Binary files /dev/null and b/screenshot/image.jpg differ