django-drf-blog-api 0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-drf-blog-api might be problematic. Click here for more details.
- django-drf-blog-api-0.2/LICENSE +19 -0
- django-drf-blog-api-0.2/MANIFEST.in +2 -0
- django-drf-blog-api-0.2/PKG-INFO +55 -0
- django-drf-blog-api-0.2/README.rst +27 -0
- django-drf-blog-api-0.2/django_drf_blog_api/__init__.py +0 -0
- django-drf-blog-api-0.2/django_drf_blog_api/admin.py +38 -0
- django-drf-blog-api-0.2/django_drf_blog_api/apps.py +9 -0
- django-drf-blog-api-0.2/django_drf_blog_api/forms.py +39 -0
- django-drf-blog-api-0.2/django_drf_blog_api/migrations/__init__.py +0 -0
- django-drf-blog-api-0.2/django_drf_blog_api/models.py +147 -0
- django-drf-blog-api-0.2/django_drf_blog_api/serializer.py +76 -0
- django-drf-blog-api-0.2/django_drf_blog_api/signals.py +11 -0
- django-drf-blog-api-0.2/django_drf_blog_api/tests.py +3 -0
- django-drf-blog-api-0.2/django_drf_blog_api/urls.py +11 -0
- django-drf-blog-api-0.2/django_drf_blog_api/utils.py +32 -0
- django-drf-blog-api-0.2/django_drf_blog_api/views.py +157 -0
- django-drf-blog-api-0.2/django_drf_blog_api.egg-info/PKG-INFO +55 -0
- django-drf-blog-api-0.2/django_drf_blog_api.egg-info/SOURCES.txt +23 -0
- django-drf-blog-api-0.2/django_drf_blog_api.egg-info/dependency_links.txt +1 -0
- django-drf-blog-api-0.2/django_drf_blog_api.egg-info/requires.txt +1 -0
- django-drf-blog-api-0.2/django_drf_blog_api.egg-info/top_level.txt +1 -0
- django-drf-blog-api-0.2/pyproject.toml +3 -0
- django-drf-blog-api-0.2/setup.cfg +36 -0
- django-drf-blog-api-0.2/setup.py +13 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2024 Osiota Samuel Obrozie
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django-drf-blog-api
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: A Django app blog API endpoints.
|
|
5
|
+
Home-page: https://www.example.com/
|
|
6
|
+
Author: Osiota Samuel Obrozie
|
|
7
|
+
Author-email: osiotaobrozie@gmail.com
|
|
8
|
+
License: BSD-3-Clause # Example license
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 5.0
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# django-drf-blog-api
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
django-drf-blog-api is a Django blog API app
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
1. Add "Blog" to your INSTALLED_APPS setting like this::
|
|
40
|
+
|
|
41
|
+
INSTALLED_APPS = [
|
|
42
|
+
...,
|
|
43
|
+
"django-drf-blog-api",
|
|
44
|
+
]
|
|
45
|
+
```
|
|
46
|
+
2. Include the polls URLconf in your project urls.py like this::
|
|
47
|
+
|
|
48
|
+
path("blog/", include("django-drf-blog-api.urls")),
|
|
49
|
+
|
|
50
|
+
3. Run ``python manage.py migrate`` to create the models.
|
|
51
|
+
|
|
52
|
+
4. Start the development server and visit the admin to create a poll.
|
|
53
|
+
|
|
54
|
+
5. Visit the ``/blog/`` URL to participate in the poll.
|
|
55
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
# django-drf-blog-api
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
django-drf-blog-api is a Django blog API app
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
1. Add "Blog" to your INSTALLED_APPS setting like this::
|
|
13
|
+
|
|
14
|
+
INSTALLED_APPS = [
|
|
15
|
+
...,
|
|
16
|
+
"django-drf-blog-api",
|
|
17
|
+
]
|
|
18
|
+
```
|
|
19
|
+
2. Include the polls URLconf in your project urls.py like this::
|
|
20
|
+
|
|
21
|
+
path("blog/", include("django-drf-blog-api.urls")),
|
|
22
|
+
|
|
23
|
+
3. Run ``python manage.py migrate`` to create the models.
|
|
24
|
+
|
|
25
|
+
4. Start the development server and visit the admin to create a poll.
|
|
26
|
+
|
|
27
|
+
5. Visit the ``/blog/`` URL to participate in the poll.
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from . models import *
|
|
4
|
+
from django.contrib.auth.admin import UserAdmin
|
|
5
|
+
from .models import UserAccount
|
|
6
|
+
from .forms import UserChangeForm, UserCreationForm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CustomUserAdmin(UserAdmin):
|
|
10
|
+
form = UserChangeForm
|
|
11
|
+
add_form = UserCreationForm
|
|
12
|
+
ordering = ('email',)
|
|
13
|
+
list_display = ('first_name', 'last_name', 'email',
|
|
14
|
+
'is_featured', 'is_active', 'is_staff', 'is_superuser')
|
|
15
|
+
model = UserAccount
|
|
16
|
+
fieldsets = (
|
|
17
|
+
(None, {'fields': ('image', 'password',)}),
|
|
18
|
+
('Personal info', {
|
|
19
|
+
'fields': ('first_name', 'last_name', 'email', 'gender', 'phone_number', 'date_of_birth',)}),
|
|
20
|
+
('Featured Athlete', {
|
|
21
|
+
'fields': ('is_featured',)}),
|
|
22
|
+
('Contact Address', {
|
|
23
|
+
'fields': ('home_address', 'local_govt', 'state_of_origin', 'nationality',)}),
|
|
24
|
+
('Permissions', {
|
|
25
|
+
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
|
26
|
+
('Important dates', {
|
|
27
|
+
'fields': ('date_joined', 'last_login',)}),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
add_fieldsets = (
|
|
31
|
+
(None, {
|
|
32
|
+
'classes': ('wide',),
|
|
33
|
+
'fields': ('first_name', 'last_name', 'email', 'phone_number', 'password1', 'password2')}
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
admin.site.register(UserAccount, CustomUserAdmin)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
|
3
|
+
|
|
4
|
+
from .models import UserAccount
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserCreationForm(forms.ModelForm):
|
|
8
|
+
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
|
|
9
|
+
password2 = forms.CharField(
|
|
10
|
+
label='Password confirmation', widget=forms.PasswordInput)
|
|
11
|
+
|
|
12
|
+
class Meta:
|
|
13
|
+
model = UserAccount
|
|
14
|
+
fields = ("first_name", "last_name", "phone_number", "email")
|
|
15
|
+
|
|
16
|
+
def clean_password2(self):
|
|
17
|
+
password1 = self.cleaned_data.get("password1")
|
|
18
|
+
password2 = self.cleaned_data.get("password2")
|
|
19
|
+
if password1 and password2 and password1 != password2:
|
|
20
|
+
raise forms.ValidationError("Passwords don't match")
|
|
21
|
+
return password2
|
|
22
|
+
|
|
23
|
+
def save(self, commit=True):
|
|
24
|
+
user = super().save(commit=False)
|
|
25
|
+
user.set_password(self.cleaned_data["password1"])
|
|
26
|
+
if commit:
|
|
27
|
+
user.save()
|
|
28
|
+
return user
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UserChangeForm(forms.ModelForm):
|
|
32
|
+
password = ReadOnlyPasswordHashField()
|
|
33
|
+
|
|
34
|
+
class Meta:
|
|
35
|
+
model = UserAccount
|
|
36
|
+
fields = '__all__'
|
|
37
|
+
|
|
38
|
+
def clean_password(self):
|
|
39
|
+
return self.initial["password"]
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
|
|
3
|
+
from django.core.validators import RegexValidator
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from cloudinary.models import CloudinaryField
|
|
6
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
7
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UserAccountManager(BaseUserManager):
|
|
11
|
+
use_in_migrations = True
|
|
12
|
+
|
|
13
|
+
def _create_user(self, email, password, **extra_fields):
|
|
14
|
+
"""
|
|
15
|
+
Creates and saves a User with the given email and password.
|
|
16
|
+
"""
|
|
17
|
+
if not email:
|
|
18
|
+
raise ValueError('Users must have an email address')
|
|
19
|
+
|
|
20
|
+
email = self.normalize_email(email)
|
|
21
|
+
user = self.model(email=email, **extra_fields)
|
|
22
|
+
user.set_password(password)
|
|
23
|
+
user.save(using=self._db)
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
def create_user(self, email, password=None, **extra_fields):
|
|
27
|
+
return self._create_user(email, password, **extra_fields)
|
|
28
|
+
|
|
29
|
+
def create_superuser(self, email, password, **extra_fields):
|
|
30
|
+
|
|
31
|
+
extra_fields.setdefault('is_superuser', True)
|
|
32
|
+
extra_fields.setdefault('is_staff', True)
|
|
33
|
+
|
|
34
|
+
if extra_fields.get('is_superuser') is not True:
|
|
35
|
+
raise ValueError('Superuser must have is_superuser=True.')
|
|
36
|
+
|
|
37
|
+
return self._create_user(email, password, **extra_fields)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UserAccount(AbstractBaseUser, PermissionsMixin):
|
|
41
|
+
GENDER = [
|
|
42
|
+
('Male', 'Male'),
|
|
43
|
+
('Female', 'Female')
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
USER_TYPE = [
|
|
47
|
+
('Author', 'Author'),
|
|
48
|
+
('Public', 'Public')
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
email = models.EmailField(max_length=255, unique=True)
|
|
52
|
+
first_name = models.CharField(max_length=255)
|
|
53
|
+
last_name = models.CharField(max_length=255)
|
|
54
|
+
account_type = models.CharField(
|
|
55
|
+
max_length=255, blank=True, choices=USER_TYPE, default='Public')
|
|
56
|
+
phone_number = models.CharField(null=True, blank=True, max_length=11, validators=[
|
|
57
|
+
RegexValidator(r'^\d{11}$', 'Enter a valid phone number.')])
|
|
58
|
+
date_of_birth = models.DateField(null=True, blank=True)
|
|
59
|
+
is_active = models.BooleanField(default=True)
|
|
60
|
+
is_staff = models.BooleanField(default=False)
|
|
61
|
+
is_superuser = models.BooleanField(default=False)
|
|
62
|
+
date_joined = models.DateTimeField(null=True, blank=True)
|
|
63
|
+
image = CloudinaryField('image', null=True, blank=True)
|
|
64
|
+
gender = models.CharField(
|
|
65
|
+
max_length=255, null=True, blank=True, choices=GENDER)
|
|
66
|
+
home_address = models.TextField(null=True, blank=True)
|
|
67
|
+
local_govt = models.CharField(max_length=255, null=True, blank=True)
|
|
68
|
+
state_of_origin = models.CharField(max_length=255, null=True, blank=True)
|
|
69
|
+
nationality = models.CharField(max_length=255, null=True, blank=True)
|
|
70
|
+
|
|
71
|
+
objects = UserAccountManager()
|
|
72
|
+
|
|
73
|
+
USERNAME_FIELD = 'email'
|
|
74
|
+
REQUIRED_FIELDS = ['first_name', 'last_name', 'phone_number']
|
|
75
|
+
ordering = ('email',)
|
|
76
|
+
|
|
77
|
+
def get_full_name(self):
|
|
78
|
+
return f"{self.first_name} {self.last_name}"
|
|
79
|
+
|
|
80
|
+
def get_short_name(self):
|
|
81
|
+
return self.first_name
|
|
82
|
+
|
|
83
|
+
def get_image_url(self):
|
|
84
|
+
return (f"https://res.cloudinary.com/dkcjpdk1c/image/upload/{self.image}")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def get_photo_url(self):
|
|
88
|
+
if self.image and hasattr(self.image, 'url'):
|
|
89
|
+
return self.image.url
|
|
90
|
+
else:
|
|
91
|
+
return "https://cdn-icons-png.flaticon.com/512/147/147142.png"
|
|
92
|
+
|
|
93
|
+
def __str__(self):
|
|
94
|
+
return self.email
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Category(models.Model):
|
|
98
|
+
name = models.CharField(max_length=50)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Tag(models.Model):
|
|
102
|
+
name = models.CharField(max_length=50)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Keyword(models.Model):
|
|
106
|
+
name = models.CharField(max_length=50)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Post(models.Model):
|
|
110
|
+
title = models.CharField(max_length=200)
|
|
111
|
+
content = models.TextField()
|
|
112
|
+
pub_date = models.DateTimeField(auto_now_add=True)
|
|
113
|
+
author = models.ForeignKey(
|
|
114
|
+
settings.AUTH_USER_MODEL, related_name='post_author', on_delete=models.CASCADE)
|
|
115
|
+
category = models.ForeignKey(
|
|
116
|
+
Category, related_name='post_category', on_delete=models.SET_NULL, null=True)
|
|
117
|
+
tags = models.ManyToManyField(Tag, related_name='post_tag')
|
|
118
|
+
keywords = models.ManyToManyField(Keyword, related_name='post_keyword')
|
|
119
|
+
slug = models.SlugField(max_length=250, blank=True, null=True)
|
|
120
|
+
|
|
121
|
+
def __str__(self):
|
|
122
|
+
return self.title
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Comment(models.Model):
|
|
126
|
+
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
|
127
|
+
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='comment_user',
|
|
128
|
+
on_delete=models.CASCADE)
|
|
129
|
+
comment = models.TextField()
|
|
130
|
+
parent = models.ForeignKey(
|
|
131
|
+
'self', null=True, blank=True, related_name='replies', on_delete=models.CASCADE)
|
|
132
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return f"Comment by {self.user} on {self.post} at {self.created_at}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Like(models.Model):
|
|
139
|
+
user = models.ForeignKey(settings.AUTH_USER_MODEL,
|
|
140
|
+
on_delete=models.CASCADE)
|
|
141
|
+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
142
|
+
object_id = models.PositiveIntegerField()
|
|
143
|
+
content_object = GenericForeignKey('content_type', 'object_id')
|
|
144
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
145
|
+
|
|
146
|
+
def __str__(self):
|
|
147
|
+
return f"Like by {self.user} on {self.content_object} at {self.created_at}"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import *
|
|
3
|
+
from djoser.serializers import UserCreateSerializer, UserSerializer
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
|
|
6
|
+
user = get_user_model()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserCreateSerializer(UserCreateSerializer):
|
|
10
|
+
class Meta(UserCreateSerializer.Meta):
|
|
11
|
+
model = user
|
|
12
|
+
fields = ('id', 'first_name', 'last_name',
|
|
13
|
+
'phone_number', 'email', 'password')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserInfoSerializer(UserSerializer):
|
|
17
|
+
class Meta(UserSerializer.Meta):
|
|
18
|
+
model = user
|
|
19
|
+
fields = ('id', 'first_name', 'last_name', 'phone_number', 'email', 'get_photo_url', 'date_of_birth',
|
|
20
|
+
'gender', 'home_address', 'local_govt', 'state_of_origin', 'nationality', 'image',)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CategorySerializer(serializers.ModelSerializer):
|
|
24
|
+
class Meta:
|
|
25
|
+
model = Category
|
|
26
|
+
fields = "__all__"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TagSerializer(serializers.ModelSerializer):
|
|
30
|
+
class Meta:
|
|
31
|
+
model = Tag
|
|
32
|
+
fields = "__all__"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class KeywordSerializer(serializers.ModelSerializer):
|
|
36
|
+
class Meta:
|
|
37
|
+
model = Keyword
|
|
38
|
+
fields = "__all__"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CommentSerializer(serializers.ModelSerializer):
|
|
42
|
+
user = UserInfoSerializer()
|
|
43
|
+
replies = serializers.SerializerMethodField()
|
|
44
|
+
|
|
45
|
+
class Meta:
|
|
46
|
+
model = Comment
|
|
47
|
+
fields = ('id', 'user', 'comment', 'created_at', 'replies')
|
|
48
|
+
|
|
49
|
+
def get_replies(self, obj):
|
|
50
|
+
replies = obj.replies.all()
|
|
51
|
+
return CommentSerializer(replies, many=True).data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PostSerializer(serializers.ModelSerializer):
|
|
55
|
+
category = CategorySerializer()
|
|
56
|
+
comments = CommentSerializer()
|
|
57
|
+
tags = TagSerializer(many=True)
|
|
58
|
+
Keywords = KeywordSerializer(many=True)
|
|
59
|
+
author = UserInfoSerializer()
|
|
60
|
+
|
|
61
|
+
class Meta:
|
|
62
|
+
model = Post
|
|
63
|
+
fields = ('id', 'content', 'pub_date', 'slug',
|
|
64
|
+
'category', 'tags', 'keywords', 'comments', 'author')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LikeSerializer(serializers.ModelSerializer):
|
|
68
|
+
user = UserInfoSerializer()
|
|
69
|
+
content_object = serializers.SerializerMethodField()
|
|
70
|
+
|
|
71
|
+
class Meta:
|
|
72
|
+
model = Like
|
|
73
|
+
fields = ('id', 'user', 'content_object', 'created_at')
|
|
74
|
+
|
|
75
|
+
def get_content_object(self, obj):
|
|
76
|
+
return str(obj.content_object)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.db.models.signals import pre_save
|
|
2
|
+
from .utils import unique_slug_generator
|
|
3
|
+
from .models import *
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def slug_generator(sender, instance, *args, **kwargs):
|
|
7
|
+
if not instance.slug:
|
|
8
|
+
instance.slug = unique_slug_generator(instance)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
pre_save.connect(slug_generator, sender=Post)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from .views import *
|
|
3
|
+
|
|
4
|
+
urlpatterns = [
|
|
5
|
+
path('category', CategoryView.as_view()),
|
|
6
|
+
path('post-list', PostListView.as_view()), # Public users
|
|
7
|
+
path('post-list/<slug:slug>', PostDetailView.as_view()), # Public users
|
|
8
|
+
path('post', PostView.as_view()), # Authors
|
|
9
|
+
path('comment', CommentView.as_view()),
|
|
10
|
+
path('like', LikeView.as_view()),
|
|
11
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import string
|
|
3
|
+
|
|
4
|
+
from django.utils.text import slugify
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
|
|
8
|
+
return ''.join(random.choice(chars) for _ in range(size))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def unique_slug_generator(instance, new_slug=None):
|
|
12
|
+
"""
|
|
13
|
+
This is for a Django project and it assumes your instance
|
|
14
|
+
has a model with a slug field and a title character (char) field.
|
|
15
|
+
"""
|
|
16
|
+
if new_slug is not None:
|
|
17
|
+
slug = new_slug
|
|
18
|
+
else:
|
|
19
|
+
try:
|
|
20
|
+
slug = slugify(instance.title)
|
|
21
|
+
except AttributeError:
|
|
22
|
+
slug = slugify(instance.name)
|
|
23
|
+
|
|
24
|
+
Klass = instance.__class__
|
|
25
|
+
qs_exists = Klass.objects.filter(slug=slug).exists()
|
|
26
|
+
if qs_exists:
|
|
27
|
+
new_slug = "{slug}-{randstr}".format(
|
|
28
|
+
slug=slug,
|
|
29
|
+
randstr=random_string_generator(size=4)
|
|
30
|
+
)
|
|
31
|
+
return unique_slug_generator(instance, new_slug=new_slug)
|
|
32
|
+
return slug
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from django.shortcuts import render
|
|
2
|
+
from rest_framework import generics
|
|
3
|
+
from .models import *
|
|
4
|
+
from .serializer import *
|
|
5
|
+
from rest_framework.decorators import api_view
|
|
6
|
+
from rest_framework.response import Response
|
|
7
|
+
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
|
8
|
+
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
9
|
+
from rest_framework_simplejwt.authentication import JWTAuthentication
|
|
10
|
+
from djoser.views import UserViewSet
|
|
11
|
+
from rest_framework.views import APIView
|
|
12
|
+
from rest_framework import status
|
|
13
|
+
from django.contrib.contenttypes.models import ContentType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PostListView(generics.ListAPIView): # Public users
|
|
17
|
+
serializer_class = PostSerializer
|
|
18
|
+
queryset = Post.objects.all()
|
|
19
|
+
permission_classes = [AllowAny,]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PostDetailView(generics.RetrieveAPIView):
|
|
23
|
+
lookup_field = 'slug'
|
|
24
|
+
queryset = Post.objects.all()
|
|
25
|
+
serializer_class = PostSerializer
|
|
26
|
+
permission_classes = [AllowAny,]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CategoryView(generics.ListAPIView):
|
|
30
|
+
queryset = Category.objects.all()
|
|
31
|
+
serializer_class = CategorySerializer
|
|
32
|
+
permission_classes = [AllowAny,]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PostView(APIView): # Authors
|
|
36
|
+
permission_classes = [IsAuthenticated,]
|
|
37
|
+
|
|
38
|
+
def get(self, request, *args, **kwargs):
|
|
39
|
+
posts = Post.objects.filter(user=self.request.user)
|
|
40
|
+
serializer = PostSerializer(posts, many=True)
|
|
41
|
+
return Response(serializer.data)
|
|
42
|
+
|
|
43
|
+
def put(self, request, *args, **kwargs):
|
|
44
|
+
post_id = request.data.get('id')
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
post = Post.objects.get(
|
|
48
|
+
id=post_id, user=self.request.user)
|
|
49
|
+
except Post.DoesNotExist:
|
|
50
|
+
return Response({'error': 'Post not found.'}, status=404)
|
|
51
|
+
|
|
52
|
+
serializer = PostSerializer(post, data=request.data)
|
|
53
|
+
if serializer.is_valid():
|
|
54
|
+
serializer.save()
|
|
55
|
+
return Response(serializer.data, status=200)
|
|
56
|
+
|
|
57
|
+
return Response(serializer.errors, status=400)
|
|
58
|
+
|
|
59
|
+
def post(self, request, *args, **kwargs):
|
|
60
|
+
title = request.data.get('title')
|
|
61
|
+
content = request.data.get('content')
|
|
62
|
+
category = request.data.get('category')
|
|
63
|
+
tags = request.data.get('tags')
|
|
64
|
+
keywords = request.data.get('keywords')
|
|
65
|
+
|
|
66
|
+
if not title or not content or not category:
|
|
67
|
+
return Response({'error': 'Title, Content and Category are required fields.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
post = Post.objects.create(
|
|
71
|
+
user=request.user, title=title, content=content, category=category, tags=tags, keywords=keywords)
|
|
72
|
+
|
|
73
|
+
serializer = PostSerializer(post)
|
|
74
|
+
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
77
|
+
|
|
78
|
+
def delete(self, request, *args, **kwargs):
|
|
79
|
+
post_id = request.data.get('id')
|
|
80
|
+
try:
|
|
81
|
+
post = Post.objects.get(id=post_id)
|
|
82
|
+
post.delete()
|
|
83
|
+
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
84
|
+
except Post.DoesNotExist:
|
|
85
|
+
return Response({'error': 'Post not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CommentView(APIView):
|
|
89
|
+
permission_classes = [IsAuthenticated,]
|
|
90
|
+
|
|
91
|
+
def get(self, request, *args, **kwargs):
|
|
92
|
+
# Get the post_id from query parameters
|
|
93
|
+
post_id = request.GET.get('post_id')
|
|
94
|
+
if post_id is None:
|
|
95
|
+
return Response({"error": "Missing 'post_id' query parameter."}, status=400)
|
|
96
|
+
|
|
97
|
+
comments = Comment.objects.filter(post_id=post_id)
|
|
98
|
+
serializer = PostSerializer(comments, many=True)
|
|
99
|
+
return Response(serializer.data)
|
|
100
|
+
|
|
101
|
+
def put(self, request, *args, **kwargs):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def post(self, request, *args, **kwargs):
|
|
105
|
+
comment = request.data.get('comment')
|
|
106
|
+
post_id = request.data.get('post')
|
|
107
|
+
parent_comment_id = request.data.get('parent_comment')
|
|
108
|
+
|
|
109
|
+
if not comment or not parent_comment_id or not post_id:
|
|
110
|
+
return Response({'error': 'Comment and Post_ID are required fields.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
post = Post.objects.get(id=post_id)
|
|
114
|
+
|
|
115
|
+
if parent_comment_id:
|
|
116
|
+
parent_comment = Comment.objects.get(id=parent_comment_id)
|
|
117
|
+
comment = Comment.objects.create(
|
|
118
|
+
user=request.user, comment=comment, post=post, parent=parent_comment)
|
|
119
|
+
else:
|
|
120
|
+
comment = Comment.objects.create(
|
|
121
|
+
user=request.user, comment=comment, post=post)
|
|
122
|
+
|
|
123
|
+
serializer = CommentSerializer(comment)
|
|
124
|
+
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
127
|
+
|
|
128
|
+
def delete(self, request, *args, **kwargs):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class LikeView(APIView):
|
|
133
|
+
permission_classes = [IsAuthenticated]
|
|
134
|
+
|
|
135
|
+
def post(self, request, *args, **kwargs):
|
|
136
|
+
content_type = request.data.get('content_type')
|
|
137
|
+
object_id = request.data.get('object_id')
|
|
138
|
+
|
|
139
|
+
if not content_type or not object_id:
|
|
140
|
+
return Response({'error': 'content_type and object_id are required fields.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
content_type = ContentType.objects.get(model=content_type)
|
|
144
|
+
except ContentType.DoesNotExist:
|
|
145
|
+
return Response({'error': 'Invalid content_type.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
146
|
+
|
|
147
|
+
like, created = Like.objects.get_or_create(
|
|
148
|
+
user=request.user,
|
|
149
|
+
content_type=content_type,
|
|
150
|
+
object_id=object_id
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if created:
|
|
154
|
+
return Response({'message': 'Liked successfully.'}, status=status.HTTP_201_CREATED)
|
|
155
|
+
else:
|
|
156
|
+
like.delete()
|
|
157
|
+
return Response({'message': 'Unliked successfully.'}, status=status.HTTP_200_OK)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django-drf-blog-api
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: A Django app blog API endpoints.
|
|
5
|
+
Home-page: https://www.example.com/
|
|
6
|
+
Author: Osiota Samuel Obrozie
|
|
7
|
+
Author-email: osiotaobrozie@gmail.com
|
|
8
|
+
License: BSD-3-Clause # Example license
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 5.0
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# django-drf-blog-api
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
django-drf-blog-api is a Django blog API app
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
1. Add "Blog" to your INSTALLED_APPS setting like this::
|
|
40
|
+
|
|
41
|
+
INSTALLED_APPS = [
|
|
42
|
+
...,
|
|
43
|
+
"django-drf-blog-api",
|
|
44
|
+
]
|
|
45
|
+
```
|
|
46
|
+
2. Include the polls URLconf in your project urls.py like this::
|
|
47
|
+
|
|
48
|
+
path("blog/", include("django-drf-blog-api.urls")),
|
|
49
|
+
|
|
50
|
+
3. Run ``python manage.py migrate`` to create the models.
|
|
51
|
+
|
|
52
|
+
4. Start the development server and visit the admin to create a poll.
|
|
53
|
+
|
|
54
|
+
5. Visit the ``/blog/`` URL to participate in the poll.
|
|
55
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.rst
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.cfg
|
|
6
|
+
setup.py
|
|
7
|
+
django_drf_blog_api/__init__.py
|
|
8
|
+
django_drf_blog_api/admin.py
|
|
9
|
+
django_drf_blog_api/apps.py
|
|
10
|
+
django_drf_blog_api/forms.py
|
|
11
|
+
django_drf_blog_api/models.py
|
|
12
|
+
django_drf_blog_api/serializer.py
|
|
13
|
+
django_drf_blog_api/signals.py
|
|
14
|
+
django_drf_blog_api/tests.py
|
|
15
|
+
django_drf_blog_api/urls.py
|
|
16
|
+
django_drf_blog_api/utils.py
|
|
17
|
+
django_drf_blog_api/views.py
|
|
18
|
+
django_drf_blog_api.egg-info/PKG-INFO
|
|
19
|
+
django_drf_blog_api.egg-info/SOURCES.txt
|
|
20
|
+
django_drf_blog_api.egg-info/dependency_links.txt
|
|
21
|
+
django_drf_blog_api.egg-info/requires.txt
|
|
22
|
+
django_drf_blog_api.egg-info/top_level.txt
|
|
23
|
+
django_drf_blog_api/migrations/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Django>=5.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_drf_blog_api
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = django-drf-blog-api
|
|
3
|
+
version = 0.2
|
|
4
|
+
description = A Django app blog API endpoints.
|
|
5
|
+
long_description = file: README.rst
|
|
6
|
+
url = https://www.example.com/
|
|
7
|
+
author = Osiota Samuel Obrozie
|
|
8
|
+
author_email = osiotaobrozie@gmail.com
|
|
9
|
+
license = BSD-3-Clause # Example license
|
|
10
|
+
classifiers =
|
|
11
|
+
Environment :: Web Environment
|
|
12
|
+
Framework :: Django
|
|
13
|
+
Framework :: Django :: 5.0
|
|
14
|
+
Intended Audience :: Developers
|
|
15
|
+
License :: OSI Approved :: BSD License
|
|
16
|
+
Operating System :: OS Independent
|
|
17
|
+
Programming Language :: Python
|
|
18
|
+
Programming Language :: Python :: 3
|
|
19
|
+
Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Programming Language :: Python :: 3.10
|
|
21
|
+
Programming Language :: Python :: 3.11
|
|
22
|
+
Programming Language :: Python :: 3.12
|
|
23
|
+
Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
|
+
|
|
26
|
+
[options]
|
|
27
|
+
include_package_data = true
|
|
28
|
+
packages = find:
|
|
29
|
+
python_requires = >=3.10
|
|
30
|
+
install_requires =
|
|
31
|
+
Django >= 5.0
|
|
32
|
+
|
|
33
|
+
[egg_info]
|
|
34
|
+
tag_build =
|
|
35
|
+
tag_date = 0
|
|
36
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
# read the contents of your README file
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
this_directory = Path(__file__).parent
|
|
6
|
+
long_description = (this_directory / "README.rst").read_text()
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name='django-drf-blog-api',
|
|
10
|
+
# other arguments omitted
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type='text/markdown'
|
|
13
|
+
)
|