├── .gitignore ├── Dockerfile ├── account ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-310.pyc │ ├── admin.cpython-310.pyc │ ├── apps.cpython-310.pyc │ └── models.cpython-310.pyc ├── admin.py ├── apps.py ├── migrations │ ├── __init__.py │ └── __pycache__ │ │ └── __init__.cpython-310.pyc ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── blog_app ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-310.pyc │ ├── admin.cpython-310.pyc │ ├── apps.cpython-310.pyc │ ├── models.cpython-310.pyc │ ├── urls.cpython-310.pyc │ └── views.cpython-310.pyc ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_blog_author.py │ ├── 0003_remove_blog_author.py │ ├── 0004_category_rename_description_blog_blog_description_and_more.py │ ├── 0005_blog_author.py │ ├── __init__.py │ └── __pycache__ │ │ └── __init__.cpython-310.pyc ├── models.py ├── pagination.py ├── permissions.py ├── serializers.py ├── tests.py ├── throttle.py ├── urls.py └── views.py ├── blog_rest_api ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-310.pyc │ ├── settings.cpython-310.pyc │ ├── urls.cpython-310.pyc │ └── wsgi.cpython-310.pyc ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── docker-compose.yml ├── manage.py ├── readme.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/django 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=django 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | db.sqlite3-journal 12 | media 13 | 14 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 15 | # in your Git repository. Update and uncomment the following line accordingly. 16 | # /staticfiles/ 17 | 18 | ### Django.Python Stack ### 19 | # Byte-compiled / optimized / DLL files 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | *.py,cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | cover/ 70 | 71 | # Translations 72 | *.mo 73 | 74 | # Django stuff: 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | # For a library or package, you might want to ignore these files since the code is 99 | # intended to run in multiple environments; otherwise, check them in: 100 | # .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # poetry 110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 111 | # This is especially recommended for binary packages to ensure reproducibility, and is more 112 | # commonly ignored for libraries. 113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 114 | #poetry.lock 115 | 116 | # pdm 117 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 118 | #pdm.lock 119 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 120 | # in version control. 121 | # https://pdm.fming.dev/#use-with-ide 122 | .pdm.toml 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | # End of https://www.toptal.com/developers/gitignore/api/django -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | RUN mkdir /blog_api 5 | WORKDIR /blog_api 6 | COPY . /blog_api/ 7 | RUN pip install -r requirements.txt -------------------------------------------------------------------------------- /account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/__init__.py -------------------------------------------------------------------------------- /account/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /account/__pycache__/admin.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/__pycache__/admin.cpython-310.pyc -------------------------------------------------------------------------------- /account/__pycache__/apps.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/__pycache__/apps.cpython-310.pyc -------------------------------------------------------------------------------- /account/__pycache__/models.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/__pycache__/models.cpython-310.pyc -------------------------------------------------------------------------------- /account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'account' 7 | -------------------------------------------------------------------------------- /account/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/migrations/__init__.py -------------------------------------------------------------------------------- /account/migrations/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/account/migrations/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /account/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.signals import post_save 3 | from django.dispatch import receiver 4 | from rest_framework.authtoken.models import Token 5 | from django.conf import settings 6 | 7 | @receiver(post_save, sender=settings.AUTH_USER_MODEL) 8 | def create_auth_token(sender, instance=None, created=False, **kwargs): 9 | if created: 10 | Token.objects.create(user=instance) -------------------------------------------------------------------------------- /account/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import serializers 3 | 4 | class UserRegisterSerializer(serializers.ModelSerializer): 5 | password2 = serializers.CharField(style={'input_type': 'password'}, write_only=True) 6 | class Meta: 7 | model = User 8 | fields = ['username', 'email', 'password', 'password2'] 9 | extra_kwargs = { 10 | 'password': {'write_only': True} 11 | } 12 | 13 | def save(self): 14 | password = self.validated_data['password'] 15 | password2 = self.validated_data['password2'] 16 | 17 | if password != password2: 18 | raise serializers.ValidationError({"Error": "Password Does not match"}) 19 | 20 | if User.objects.filter(email = self.validated_data['email']).exists(): 21 | raise serializers.ValidationError({"Error": "Email already exist"}) 22 | 23 | account = User(email=self.validated_data['email'], username=self.validated_data['username']) 24 | account.set_password(password) 25 | account.save() 26 | 27 | return account -------------------------------------------------------------------------------- /account/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /account/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework.authtoken.views import obtain_auth_token 3 | from . import views 4 | from rest_framework_simplejwt.views import ( 5 | TokenObtainPairView, 6 | TokenRefreshView, 7 | ) 8 | 9 | urlpatterns = [ 10 | path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 11 | path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 12 | 13 | path("login/", obtain_auth_token, name="login"), 14 | path("logout_user/", views.logout_user, name="logout_user"), 15 | path("register/", views.user_register_view, name="register"), 16 | ] 17 | -------------------------------------------------------------------------------- /account/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from rest_framework.decorators import api_view 3 | from rest_framework.response import Response 4 | from rest_framework import status 5 | from rest_framework.authtoken.models import Token 6 | from .serializers import UserRegisterSerializer 7 | from rest_framework_simplejwt.tokens import RefreshToken 8 | 9 | @api_view(["POST",]) 10 | def logout_user(request): 11 | if request.method == "POST": 12 | request.user.auth_token.delete() 13 | return Response({"Message": "You are logged out"}, status=status.HTTP_200_OK) 14 | 15 | @api_view(["POST",]) 16 | def user_register_view(request): 17 | if request.method == "POST": 18 | serializer = UserRegisterSerializer(data=request.data) 19 | 20 | data = {} 21 | 22 | if serializer.is_valid(): 23 | account = serializer.save() 24 | 25 | data['response'] = 'Account has been created' 26 | data['username'] = account.username 27 | data['email'] = account.email 28 | 29 | # token = Token.objects.get(user=account).key 30 | # data['token'] = token 31 | 32 | refresh = RefreshToken.for_user(account) 33 | data['token'] = { 34 | 'refresh': str(refresh), 35 | 'access': str(refresh.access_token) 36 | } 37 | else: 38 | data = serializer.errors 39 | return Response(data) -------------------------------------------------------------------------------- /blog_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__init__.py -------------------------------------------------------------------------------- /blog_app/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/__pycache__/admin.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__pycache__/admin.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/__pycache__/apps.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__pycache__/apps.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/__pycache__/models.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__pycache__/models.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/__pycache__/urls.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__pycache__/urls.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/__pycache__/views.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/__pycache__/views.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Blog, BlogComment, Category 3 | 4 | 5 | class BlogAdmin(admin.ModelAdmin): 6 | list_display = ('id', 'blog_title', 'post_date', 'is_public') 7 | list_display_links = ('id', 'blog_title') 8 | search_fields = ('blog_title',) 9 | list_per_page = 10 10 | list_editable = ("is_public",) 11 | 12 | admin.site.register(Blog, BlogAdmin) 13 | admin.site.register(BlogComment) 14 | admin.site.register(Category) 15 | -------------------------------------------------------------------------------- /blog_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogAppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'blog_app' 7 | -------------------------------------------------------------------------------- /blog_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-10-07 16:49 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Blog', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=100)), 22 | ('description', models.TextField()), 23 | ('post_date', models.DateTimeField(auto_now_add=True)), 24 | ('is_public', models.BooleanField(default=True)), 25 | ('slug', models.CharField(blank=True, max_length=1000, null=True)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='BlogComment', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('description', models.TextField()), 33 | ('comment_date', models.DateTimeField(auto_now_add=True)), 34 | ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 35 | ('blog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog_app.blog')), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /blog_app/migrations/0002_blog_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-10-07 16:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog_app', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='blog', 15 | name='author', 16 | field=models.CharField(default='', max_length=100), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /blog_app/migrations/0003_remove_blog_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-10-07 17:22 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog_app', '0002_blog_author'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='blog', 15 | name='author', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /blog_app/migrations/0004_category_rename_description_blog_blog_description_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-10-23 13:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blog_app', '0003_remove_blog_author'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Category', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('category_name', models.CharField(max_length=50)), 19 | ], 20 | ), 21 | migrations.RenameField( 22 | model_name='blog', 23 | old_name='description', 24 | new_name='blog_description', 25 | ), 26 | migrations.RenameField( 27 | model_name='blog', 28 | old_name='name', 29 | new_name='blog_title', 30 | ), 31 | migrations.AddField( 32 | model_name='blog', 33 | name='category', 34 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='category', to='blog_app.category'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /blog_app/migrations/0005_blog_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2023-02-09 04:30 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('blog_app', '0004_category_rename_description_blog_blog_description_and_more'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='blog', 18 | name='author', 19 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /blog_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/migrations/__init__.py -------------------------------------------------------------------------------- /blog_app/migrations/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_app/migrations/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /blog_app/models.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from django.db import models 4 | from django.contrib.auth.models import User 5 | from django.template.defaultfilters import slugify 6 | 7 | # Create your models here. 8 | class Category(models.Model): 9 | category_name = models.CharField(max_length=50) 10 | 11 | def __str__(self): 12 | return self.category_name 13 | 14 | class Blog(models.Model): 15 | author = models.ForeignKey(User, on_delete=models.CASCADE) 16 | blog_title = models.CharField(max_length=100) 17 | blog_description = models.TextField() 18 | category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name="category") 19 | post_date = models.DateTimeField(auto_now_add=True) 20 | is_public = models.BooleanField(default=True) 21 | slug = models.CharField(max_length=1000, null=True, blank=True) 22 | 23 | def __str__(self): 24 | return self.blog_title 25 | 26 | def save(self, *args, **kwargs): 27 | if not self.slug: 28 | base_slug = slugify(self.blog_title + " " + self.author.username + " " + self.category.category_name) 29 | self.slug = base_slug + ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(5)) 30 | return super().save(*args, **kwargs) 31 | 32 | class BlogComment(models.Model): 33 | description = models.TextField() 34 | author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 35 | comment_date = models.DateTimeField(auto_now_add=True) 36 | blog = models.ForeignKey(Blog, on_delete=models.CASCADE) 37 | 38 | def __str__(self): 39 | return str(self.blog) -------------------------------------------------------------------------------- /blog_app/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination 2 | 3 | # class BlogListCreatePagination(PageNumberPagination): 4 | # page_size = 1 5 | # # page_query_param = 'p' 6 | # page_size_query_param = "size" 7 | # max_page_size = 50 8 | 9 | # class BlogListCreatePagination(LimitOffsetPagination): 10 | # default_limit = 2 11 | # max_limit = 10 12 | # limit_query_param = "limit" 13 | # offset_query_param = "start" 14 | 15 | class BlogListCreatePagination(CursorPagination): 16 | page_size = 2 17 | ordering = "post_date" 18 | -------------------------------------------------------------------------------- /blog_app/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | class IsAdminOrReadOnly(permissions.BasePermission): 4 | def has_permission(self, request, view): 5 | if request.method in permissions.SAFE_METHODS: 6 | return True 7 | return request.user and request.user.is_staff 8 | 9 | class IsOwnerOrReadonly(permissions.BasePermission): 10 | def has_object_permission(self, request, view, obj): 11 | if request.method in permissions.SAFE_METHODS: 12 | return True 13 | return obj.author == request.user -------------------------------------------------------------------------------- /blog_app/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from blog_app.models import Blog, Category, BlogComment 3 | from django.urls import reverse 4 | 5 | class BlogCommentSerializer(serializers.ModelSerializer): 6 | blog = serializers.StringRelatedField(read_only=True) 7 | author = serializers.ReadOnlyField(source='author.username') 8 | class Meta: 9 | model = BlogComment 10 | fields = "__all__" 11 | 12 | class BlogSerializer(serializers.ModelSerializer): 13 | comments = serializers.SerializerMethodField() 14 | author = serializers.StringRelatedField(read_only=True) 15 | category = serializers.CharField() 16 | class Meta: 17 | model = Blog 18 | fields = "__all__" 19 | 20 | def get_comments(self, obj): 21 | comments = BlogComment.objects.filter(blog=obj)[:3] 22 | request = self.context.get('request') 23 | return { 24 | "comments": BlogCommentSerializer(comments, many=True).data, 25 | "all_comment_link": request.build_absolute_uri(reverse('blog_comment_list', kwargs={'blog_id': obj.id})) 26 | } 27 | 28 | class CategorySerializer(serializers.ModelSerializer): 29 | id = serializers.IntegerField(read_only=True) 30 | category_name = serializers.CharField() 31 | category = BlogSerializer(many=True, read_only=True) 32 | class Meta: 33 | model = Category 34 | fields = "__all__" -------------------------------------------------------------------------------- /blog_app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /blog_app/throttle.py: -------------------------------------------------------------------------------- 1 | from rest_framework.throttling import UserRateThrottle 2 | 3 | class BlogListCreateViewThrottle(UserRateThrottle): 4 | scope = "blog-list" -------------------------------------------------------------------------------- /blog_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path("blog_list/", views.BlogListCreateView.as_view(), name="blog_list"), 6 | path("blog_detail//", views.BlogDetailView.as_view(), name="blog_list"), 7 | 8 | path("category_list/", views.CategoryListeCreateView.as_view(), name="category_list"), 9 | path("category_detail//", views.CategorydetailView.as_view(), name="category_detail"), 10 | 11 | path("blog_comment_list/blog//", views.BlogCommentListCreateView.as_view(), name="blog_comment_list"), 12 | path("blog_comment_detail/blog//comment//", views.BlogCommentDetailView.as_view(), name="blog_comment_detail"), 13 | ] -------------------------------------------------------------------------------- /blog_app/views.py: -------------------------------------------------------------------------------- 1 | from .models import Blog, Category, BlogComment 2 | from .serializers import BlogSerializer, CategorySerializer, BlogCommentSerializer 3 | from rest_framework.response import Response 4 | from rest_framework import status 5 | from rest_framework import generics 6 | from django.shortcuts import get_object_or_404 7 | from rest_framework import serializers 8 | from rest_framework.permissions import IsAuthenticated 9 | from .permissions import IsAdminOrReadOnly, IsOwnerOrReadonly 10 | from django_filters.rest_framework import DjangoFilterBackend 11 | from rest_framework import filters 12 | from .pagination import BlogListCreatePagination 13 | 14 | 15 | class CategoryListeCreateView(generics.ListCreateAPIView): 16 | queryset = Category.objects.all() 17 | serializer_class = CategorySerializer 18 | permission_classes = [IsAdminOrReadOnly] 19 | 20 | def list(self, request, *args, **kwargs): 21 | queryset = self.get_queryset() 22 | serializer = CategorySerializer(queryset, many=True, context={'request': request}) 23 | if queryset.exists(): 24 | return Response(serializer.data, status=status.HTTP_200_OK) 25 | else: 26 | return Response({'Message': 'No category found'}, status=status.HTTP_404_NOT_FOUND) 27 | 28 | class CategorydetailView(generics.RetrieveUpdateDestroyAPIView): 29 | queryset = Category.objects.all() 30 | serializer_class = CategorySerializer 31 | liikup_field = 'id' # slug 32 | permission_classes = [IsAdminOrReadOnly] 33 | 34 | def retrieve(self, request, *args, **kwargs): 35 | instance = self.get_object() 36 | serializer = self.get_serializer(instance) 37 | if instance: 38 | return Response(serializer.data, status=status.HTTP_200_OK) 39 | else: 40 | return Response({'Message': 'No blog Found'}, status=status.HTTP_404_NOT_FOUND) 41 | 42 | class BlogListCreateView(generics.ListCreateAPIView): 43 | queryset = Blog.objects.all() 44 | serializer_class = BlogSerializer 45 | # permission_classes = [IsAuthenticatedOrReadOnly] 46 | 47 | # UserRate & AnonRate Throttle 48 | # throttle_classes = [UserRateThrottle, AnonRateThrottle] 49 | 50 | # ScopeRateThrottle 51 | # throttle_classes = [ScopedRateThrottle] 52 | # throttle_scope = 'blog-list' 53 | 54 | # Custom Throttle 55 | # throttle_classes = [BlogListCreateViewThrottle] 56 | 57 | # Filtering 58 | filter_backends = [DjangoFilterBackend, filters.SearchFilter] 59 | filterset_fields = ['category__category_name', 'is_public'] 60 | search_fields = ['^blog_title', 'blog_description', 'category__category_name'] 61 | # ordering_fields = ['Post date', 'category__category_name'] 62 | 63 | # pagination 64 | pagination_class = BlogListCreatePagination 65 | 66 | def create(self, request, *args, **kwargs): 67 | serializer = BlogSerializer(data=request.data, context={'request': request}) 68 | serializer.is_valid(raise_exception=True) 69 | serializer.save(author=self.request.user) 70 | headers = self.get_success_headers(serializer.data) 71 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 72 | 73 | class BlogDetailView(generics.RetrieveUpdateDestroyAPIView): 74 | queryset = Blog.objects.filter(is_public = True) 75 | serializer_class = BlogSerializer 76 | liikup_field = 'id' # slug 77 | permission_classes = [IsOwnerOrReadonly] 78 | 79 | def retrieve(self, request, *args, **kwargs): 80 | instance = self.get_object() 81 | serializer = self.get_serializer(instance) 82 | if instance: 83 | return Response(serializer.data, status=status.HTTP_200_OK) 84 | else: 85 | return Response({'Message': 'No blog Found'}, status=status.HTTP_404_NOT_FOUND) 86 | 87 | class BlogCommentListCreateView(generics.ListCreateAPIView): 88 | queryset = BlogComment.objects.all() 89 | serializer_class = BlogCommentSerializer 90 | permission_classes = [IsAuthenticated] 91 | 92 | def get_queryset(self): 93 | blog_id = self.kwargs.get('blog_id') 94 | return BlogComment.objects.filter(blog_id=blog_id) 95 | 96 | def perform_create(self, serializer): 97 | blog_id = self.kwargs.get('blog_id') 98 | blog = get_object_or_404(Blog, id=blog_id) 99 | if BlogComment.objects.filter(blog=blog, author=self.request.user).exists(): 100 | raise serializers.ValidationError({'Message': 'You have already added comment on this blog'}) 101 | serializer.save(author=self.request.user, blog=blog) 102 | 103 | class BlogCommentDetailView(generics.RetrieveUpdateDestroyAPIView): 104 | queryset = BlogComment.objects.all() 105 | serializer_class = BlogCommentSerializer 106 | permission_classes = [IsOwnerOrReadonly] 107 | 108 | def get_object(self): 109 | comment_id = self.kwargs.get('comment_id') 110 | comment = get_object_or_404(BlogComment, id = comment_id) 111 | 112 | blog_id = self.kwargs.get("blog_id") 113 | if comment.blog.id != blog_id: 114 | raise serializers.ValidationError({"Message": "This comment is not related to the requested blog"}, status=status.HTTP_401_UNAUTHORIZED) 115 | return comment 116 | 117 | def delete(self, request, *args, **kwargs): 118 | comment = self.get_object() 119 | if comment.author != request.user: 120 | raise serializers.ValidationError({"Message": "You are not authorized to perform this action"}, status=status.HTTP_401_UNAUTHORIZED) 121 | return super().delete(request, *args, **kwargs) 122 | 123 | def put(self, request, *args, **kwargs): 124 | comment = self.get_object() 125 | 126 | if comment.author != request.user: 127 | raise serializers.ValidationError({"Message": "You are not authorized to perform this action"}, status=status.HTTP_401_UNAUTHORIZED) 128 | return super().put(request, *args, **kwargs) -------------------------------------------------------------------------------- /blog_rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_rest_api/__init__.py -------------------------------------------------------------------------------- /blog_rest_api/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_rest_api/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /blog_rest_api/__pycache__/settings.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_rest_api/__pycache__/settings.cpython-310.pyc -------------------------------------------------------------------------------- /blog_rest_api/__pycache__/urls.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_rest_api/__pycache__/urls.cpython-310.pyc -------------------------------------------------------------------------------- /blog_rest_api/__pycache__/wsgi.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashpatel-py/django_blog_rest_api/418bbd38f0335caee018abf3746b325f48e1fe99/blog_rest_api/__pycache__/wsgi.cpython-310.pyc -------------------------------------------------------------------------------- /blog_rest_api/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for blog_rest_api project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_rest_api.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /blog_rest_api/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for blog_rest_api project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from datetime import timedelta 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-_q751kupyqm2vx4skom+oyy559l&#=ic05$@p7ciilm#=%1t#c' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'account', 36 | 'blog_app', 37 | 'rest_framework.authtoken', 38 | 'rest_framework', 39 | 'rest_framework_simplejwt', 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'django_filters', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'blog_rest_api.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'blog_rest_api.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': BASE_DIR / 'db.sqlite3', 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 124 | 125 | STATIC_URL = 'static/' 126 | 127 | # Default primary key field type 128 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 129 | 130 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 131 | 132 | REST_FRAMEWORK = { 133 | # 'DEFAULT_PERMISSION_CLASSES': [ 134 | # 'rest_framework.permissions.IsAuthenticated', 135 | # ], 136 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 137 | # 'rest_framework.authentication.BasicAuthentication', 138 | # 'rest_framework.authentication.TokenAuthentication', 139 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 140 | ], 141 | # 'DEFAULT_THROTTLE_CLASSES': [ 142 | # 'rest_framework.throttling.AnonRateThrottle', 143 | # 'rest_framework.throttling.UserRateThrottle' 144 | # ], 145 | 'DEFAULT_THROTTLE_RATES': { 146 | 'anon': '5/day', 147 | 'user': '10/day', 148 | 'blog-list': '15/day' 149 | }, 150 | # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 151 | # 'PAGE_SIZE': 2 152 | } 153 | 154 | SIMPLE_JWT = { 155 | "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10), 156 | "REFRESH_TOKEN_LIFETIME": timedelta(days=1), 157 | } -------------------------------------------------------------------------------- /blog_rest_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('api/blog/', include('blog_app.urls')), 7 | path('api/account/', include('account.urls')), 8 | ] 9 | -------------------------------------------------------------------------------- /blog_rest_api/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blog_rest_api project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_rest_api.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | web: 5 | build: . 6 | command: python manage.py runserver 0.0.0.0:8000 7 | volumes: 8 | - .:/blog_api 9 | ports: 10 | - "8000:8000" -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_rest_api.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Blog REST API 2 | 3 | [![Youtube](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCKspdO30Fea8ZCxwg-0svOg) 4 | [![Python](https://img.shields.io/badge/Python-FFD43B?style=for-the-badge&logo=python&logoColor=blue)](https://www.youtube.com/channel/UCKspdO30Fea8ZCxwg-0svOg) 5 | [![Django](https://img.shields.io/badge/djangorest-ff1709?style=for-the-badge&logo=django&logoColor=white)](https://code.visualstudio.com/download) 6 | ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) 7 | [![VS Code](https://img.shields.io/badge/Visual_Studio_Code-0078D4?style=for-the-badge&logo=visual%20studio%20code&logoColor=white)](https://code.visualstudio.com/download) 8 | 9 | This API is based on python 100% and it is created by django rest framework. 10 | 11 | ## Steps to run the project 12 | 13 | 1. Clone the repository 14 | 2. Create a virtual environment using command `python3 -m venv env` 15 | 3. Activate virtual enviornment using command 16 | ```python 17 | # for Linux 18 | source env/bin/activate 19 | 20 | # for Windows 21 | env/Source/activavte 22 | ``` 23 | 4. Install the dependencies using command `pip install -r requirements.txt` 24 | 5. Type `source env/bin/activate` 25 | 6. Run the server using command `python manage.py makemigrations` 26 | 7. Then run the server using command `python manage.py migrate` 27 | 8. Finally run the server using command `python manage.py runserver` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.6.0 2 | Django==4.1 3 | djangorestframework==3.13.1 4 | pytz==2022.7 5 | sqlparse==0.4.3 6 | tzdata==2022.7 7 | --------------------------------------------------------------------------------