├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .gitignore ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── authentication.py │ ├── migrations │ │ └── __init__.py │ ├── mixins.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── articles │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── index.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── cfehome │ ├── __init__.py │ ├── asgi.py │ ├── routers.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── products │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── index.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_product_user.py │ │ ├── 0003_product_public.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ ├── validators.py │ ├── views.py │ └── viewsets.py └── search │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── client.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── drf.code-workspace ├── js_client ├── client.js └── index.html ├── py_client ├── basic.py ├── create.py ├── delete.py ├── detail.py ├── jwt.py ├── list.py ├── not_found.py └── update.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Coding For Entrepreneurs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django REST Framework - Tutorial 2 | Learn how to build REST APIs with Django & the Django Rest Framework. 3 | 4 | ## Coming soon 5 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/api/__init__.py -------------------------------------------------------------------------------- /backend/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'api' 7 | -------------------------------------------------------------------------------- /backend/api/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework.authentication import TokenAuthentication as BaseTokenAuth 2 | from rest_framework.authtoken.models import Token 3 | 4 | class TokenAuthentication(BaseTokenAuth): 5 | keyword = 'Token' -------------------------------------------------------------------------------- /backend/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/api/migrations/__init__.py -------------------------------------------------------------------------------- /backend/api/mixins.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | from .permissions import IsStaffEditorPermission 4 | 5 | class StaffEditorPermissionMixin(): 6 | permission_classes = [permissions.IsAdminUser, IsStaffEditorPermission] 7 | 8 | 9 | class UserQuerySetMixin(): 10 | user_field = 'user' 11 | allow_staff_view = False 12 | 13 | def get_queryset(self, *args, **kwargs): 14 | user = self.request.user 15 | lookup_data = {} 16 | lookup_data[self.user_field] = user 17 | qs = super().get_queryset(*args, **kwargs) 18 | if self.allow_staff_view and user.is_staff: 19 | return qs 20 | return qs.filter(**lookup_data) -------------------------------------------------------------------------------- /backend/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/api/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsStaffEditorPermission(permissions.DjangoModelPermissions): 5 | perms_map = { 6 | 'GET': ['%(app_label)s.view_%(model_name)s'], 7 | 'OPTIONS': [], 8 | 'HEAD': [], 9 | 'POST': ['%(app_label)s.add_%(model_name)s'], 10 | 'PUT': ['%(app_label)s.change_%(model_name)s'], 11 | 'PATCH': ['%(app_label)s.change_%(model_name)s'], 12 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'], 13 | } 14 | -------------------------------------------------------------------------------- /backend/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth import get_user_model 3 | 4 | User = get_user_model() 5 | 6 | class UserProductInlineSerializer(serializers.Serializer): 7 | url = serializers.HyperlinkedIdentityField( 8 | view_name='product-detail', 9 | lookup_field='pk', 10 | read_only=True 11 | ) 12 | title = serializers.CharField(read_only=True) 13 | 14 | 15 | class UserPublicSerializer(serializers.Serializer): 16 | username = serializers.CharField(read_only=True) 17 | this_is_not_real = serializers.CharField(read_only=True) 18 | id = serializers.IntegerField(read_only=True) 19 | # other_products = serializers.SerializerMethodField(read_only=True) 20 | 21 | # class Meta: 22 | # model = User 23 | # fields = [ 24 | # 'username', 25 | # 'this_is_not_real', 26 | # 'id' 27 | # ] 28 | 29 | # def get_other_products(self, obj): 30 | # user = obj 31 | # my_products_qs = user.product_set.all()[:5] 32 | # return UserProductInlineSerializer(my_products_qs, many=True, context=self.context).data -------------------------------------------------------------------------------- /backend/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from rest_framework.authtoken.views import obtain_auth_token 4 | from rest_framework_simplejwt.views import ( 5 | TokenObtainPairView, 6 | TokenRefreshView, 7 | TokenVerifyView, 8 | ) 9 | 10 | from . import views 11 | # from .views import api_home 12 | 13 | 14 | urlpatterns = [ 15 | path('auth/', obtain_auth_token), 16 | path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 17 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 18 | path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), 19 | path('', views.api_home), # localhost:8000/api/ 20 | # path('products/', include('products.urls')) 21 | ] 22 | -------------------------------------------------------------------------------- /backend/api/views.py: -------------------------------------------------------------------------------- 1 | from django.forms.models import model_to_dict 2 | from rest_framework.decorators import api_view 3 | from rest_framework.response import Response 4 | 5 | 6 | from products.models import Product 7 | from products.serializers import ProductSerializer 8 | 9 | @api_view(['POST']) 10 | def api_home(request, *args, **kwargs): 11 | """ 12 | DRF API View 13 | """ 14 | serializer = ProductSerializer(data=request.data) 15 | if serializer.is_valid(raise_exception=True): 16 | # instance = serializer.save() 17 | # instance = form.save() 18 | print(serializer.data) 19 | return Response(serializer.data) 20 | return Response({"invalid": "not good data"}, status=400) 21 | -------------------------------------------------------------------------------- /backend/articles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/articles/__init__.py -------------------------------------------------------------------------------- /backend/articles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Article 5 | 6 | 7 | admin.site.register(Article) -------------------------------------------------------------------------------- /backend/articles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArticlesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'articles' 7 | -------------------------------------------------------------------------------- /backend/articles/index.py: -------------------------------------------------------------------------------- 1 | from algoliasearch_django import AlgoliaIndex 2 | from algoliasearch_django.decorators import register 3 | 4 | 5 | from .models import Article 6 | 7 | 8 | @register(Article) 9 | class ArtcileIndex(AlgoliaIndex): 10 | should_index = 'is_public' 11 | fields = [ 12 | 'title', 13 | 'body', 14 | 'user', 15 | 'publish_date', 16 | 'path', 17 | 'endpoint', 18 | ] 19 | settings = { 20 | 'searchableAttributes': ['title', 'body'], 21 | 'attributesForFaceting': ['user'], 22 | 'ranking': ['asc(publish_date)'] 23 | } 24 | tags = 'get_tags_list' 25 | -------------------------------------------------------------------------------- /backend/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-21 21:53 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='Article', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=120)), 22 | ('body', models.TextField(blank=True, null=True)), 23 | ('tags', models.TextField(blank=True, help_text='Use commas to separate tags', null=True)), 24 | ('make_public', models.BooleanField(blank=True, default=False, null=True)), 25 | ('publish_date', models.DateTimeField(blank=True, null=True)), 26 | ('user', models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /backend/articles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/articles/migrations/__init__.py -------------------------------------------------------------------------------- /backend/articles/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.utils import timezone 4 | 5 | User = settings.AUTH_USER_MODEL # auth.User 6 | 7 | 8 | class ArticleManager(models.Manager): 9 | def public(self): 10 | now = timezone.now() 11 | return self.get_queryset().filter(make_public=True, publish_date__lte=now) 12 | 13 | class Article(models.Model): 14 | user = models.ForeignKey(User, default=1, null=True, on_delete=models.SET_NULL) 15 | title = models.CharField(max_length=120) 16 | body = models.TextField(blank=True, null=True) 17 | tags = models.TextField(blank=True, null=True, help_text='Use commas to separate tags') 18 | make_public = models.BooleanField(default=False, null=True, blank=True) 19 | publish_date = models.DateTimeField(auto_now=False, auto_now_add=False, blank=True, null=True) 20 | 21 | objects = ArticleManager() 22 | 23 | def get_absolute_url(self): 24 | return f"/api/articles/{self.pk}/" 25 | 26 | @property 27 | def endpoint(self): 28 | return self.get_absolute_url() 29 | 30 | @property 31 | def path(self): 32 | return f"/articles/{self.pk}/" 33 | 34 | def is_public(self): 35 | if self.publish_date is None: 36 | return False 37 | if self.make_public is None: 38 | return False 39 | now = timezone.now() 40 | is_in_past = now >= self.publish_date 41 | return is_in_past and self.make_public 42 | 43 | def get_tags_list(self): 44 | if not self.tags: 45 | return [] 46 | return [x.lower().strip() for x in self.tags.split(',')] 47 | 48 | def save(self, *args, **kwargs): 49 | if self.tags: 50 | if self.tags.endswith(","): 51 | self.tags = self.tags[:-1] 52 | if self.tags.startswith(","): 53 | self.tags = self.tags[1:] 54 | self.tags = f"{self.tags}".lower() 55 | if self.make_public and self.publish_date is None: 56 | self.publish_date = timezone.now() 57 | super().save(*args, **kwargs) 58 | -------------------------------------------------------------------------------- /backend/articles/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from api.serializers import UserPublicSerializer 4 | from .models import Article 5 | 6 | class ArticleSerializer(serializers.ModelSerializer): 7 | author = UserPublicSerializer(source='user', read_only=True) 8 | class Meta: 9 | model = Article 10 | fields = [ 11 | 'pk', 12 | 'author', 13 | 'title', 14 | 'body', 15 | 'path', 16 | 'endpoint', 17 | ] -------------------------------------------------------------------------------- /backend/articles/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | 4 | from . import views 5 | 6 | urlpatterns = [ 7 | path('', views.ArticleListView.as_view(), name='article-list'), 8 | path('/', views.ArticleDetailView.as_view(), name='article-detail'), 9 | ] -------------------------------------------------------------------------------- /backend/articles/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | # Create your views here. 3 | from .models import Article 4 | from .serializers import ArticleSerializer 5 | 6 | 7 | class ArticleListView(generics.ListAPIView): 8 | queryset = Article.objects.public() 9 | serializer_class = ArticleSerializer 10 | 11 | class ArticleDetailView(generics.RetrieveAPIView): 12 | queryset = Article.objects.public() 13 | serializer_class = ArticleSerializer -------------------------------------------------------------------------------- /backend/cfehome/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/cfehome/__init__.py -------------------------------------------------------------------------------- /backend/cfehome/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for cfehome 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/3.2/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', 'cfehome.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/cfehome/routers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | 3 | 4 | from products.viewsets import ProductGenericViewSet 5 | 6 | router = DefaultRouter() 7 | router.register('products', ProductGenericViewSet, basename='products') 8 | urlpatterns = router.urls -------------------------------------------------------------------------------- /backend/cfehome/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for cfehome project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import datetime 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-cn9t#dfjhxs%_cyenom8%qjkj=m^n(@0z85itbf+9f)o-d_13q' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | # third party api services 41 | 'algoliasearch_django', 42 | # third party packages 43 | 'corsheaders', 44 | 'rest_framework', 45 | 'rest_framework.authtoken', 46 | 'rest_framework_simplejwt', 47 | 48 | # internal apps 49 | 'api', 50 | 'articles', 51 | 'products', 52 | 'search', 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | 'django.middleware.security.SecurityMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'corsheaders.middleware.CorsMiddleware', 59 | 'django.middleware.common.CommonMiddleware', 60 | 'django.middleware.csrf.CsrfViewMiddleware', 61 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 62 | 'django.contrib.messages.middleware.MessageMiddleware', 63 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 64 | ] 65 | 66 | ROOT_URLCONF = 'cfehome.urls' 67 | CORS_URLS_REGEX = r"^/api/.*" 68 | CORS_ALLOWED_ORIGINS = [] 69 | 70 | if DEBUG: 71 | CORS_ALLOWED_ORIGINS += [ 72 | 'http://localhost:8111', 73 | 'https://localhost:8111', 74 | ] 75 | 76 | TEMPLATES = [ 77 | { 78 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 79 | 'DIRS': [], 80 | 'APP_DIRS': True, 81 | 'OPTIONS': { 82 | 'context_processors': [ 83 | 'django.template.context_processors.debug', 84 | 'django.template.context_processors.request', 85 | 'django.contrib.auth.context_processors.auth', 86 | 'django.contrib.messages.context_processors.messages', 87 | ], 88 | }, 89 | }, 90 | ] 91 | 92 | WSGI_APPLICATION = 'cfehome.wsgi.application' 93 | 94 | 95 | # Database 96 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 97 | 98 | DATABASES = { 99 | 'default': { 100 | 'ENGINE': 'django.db.backends.sqlite3', 101 | 'NAME': BASE_DIR / 'db.sqlite3', 102 | } 103 | } 104 | 105 | 106 | # Password validation 107 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 108 | 109 | AUTH_PASSWORD_VALIDATORS = [ 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 115 | }, 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 121 | }, 122 | ] 123 | 124 | 125 | # Internationalization 126 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 127 | 128 | LANGUAGE_CODE = 'en-us' 129 | 130 | TIME_ZONE = 'UTC' 131 | 132 | USE_I18N = True 133 | 134 | USE_L10N = True 135 | 136 | USE_TZ = True 137 | 138 | 139 | # Static files (CSS, JavaScript, Images) 140 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 141 | 142 | STATIC_URL = '/static/' 143 | 144 | # Default primary key field type 145 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 146 | 147 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 148 | 149 | 150 | REST_FRAMEWORK = { 151 | "DEFAULT_AUTHENTICATION_CLASSES": [ 152 | "rest_framework.authentication.SessionAuthentication", 153 | "api.authentication.TokenAuthentication", 154 | "rest_framework_simplejwt.authentication.JWTAuthentication", 155 | ], 156 | "DEFAULT_PERMISSION_CLASSES": [ 157 | "rest_framework.permissions.IsAuthenticatedOrReadOnly" 158 | ], 159 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", 160 | "PAGE_SIZE": 10 161 | } 162 | 163 | # environment variables -> django-dotenv -> reads .env 164 | ALGOLIA = { 165 | 'APPLICATION_ID': '4IHLYNCMBJ', 166 | 'API_KEY': '4b063049ede11d6f623e7cf47ef6c336', 167 | 'INDEX_PREFIX': 'cfe' 168 | } 169 | 170 | 171 | SIMPLE_JWT = { 172 | "AUTH_HEADER_TYPES": ["Bearer"], 173 | "ACCESS_TOKEN_LIFETIME": datetime.timedelta(seconds=30), # minutes=5 174 | "REFRESH_TOKEN_LIFETIME": datetime.timedelta(minutes=1), # days=1 175 | } -------------------------------------------------------------------------------- /backend/cfehome/urls.py: -------------------------------------------------------------------------------- 1 | """cfehome URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('api/', include('api.urls')), 22 | path('api/articles/', include('articles.urls')), 23 | path('api/search/', include('search.urls')), 24 | path('api/products/', include('products.urls')), 25 | path('api/v2/', include('cfehome.routers')) 26 | ] 27 | 28 | # localhost:8000/api/ 29 | -------------------------------------------------------------------------------- /backend/cfehome/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cfehome 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/3.2/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', 'cfehome.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/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', 'cfehome.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 | -------------------------------------------------------------------------------- /backend/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/products/__init__.py -------------------------------------------------------------------------------- /backend/products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Product 5 | 6 | admin.site.register(Product) -------------------------------------------------------------------------------- /backend/products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'products' 7 | -------------------------------------------------------------------------------- /backend/products/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Product 4 | 5 | class ProductForm(forms.ModelForm): 6 | class Meta: 7 | model = Product 8 | fields = [ 9 | 'title', 10 | 'content', 11 | 'price' 12 | ] -------------------------------------------------------------------------------- /backend/products/index.py: -------------------------------------------------------------------------------- 1 | from algoliasearch_django import AlgoliaIndex 2 | from algoliasearch_django.decorators import register 3 | 4 | 5 | from .models import Product 6 | 7 | 8 | @register(Product) 9 | class ProductIndex(AlgoliaIndex): 10 | # should_index = 'is_public' 11 | fields = [ 12 | 'title', 13 | 'body', 14 | 'price', 15 | 'user', 16 | 'public', 17 | 'path', 18 | 'endpoint', 19 | ] 20 | settings = { 21 | 'searchableAttributes': ['title', 'body'], 22 | 'attributesForFaceting': ['user', 'public'] 23 | } 24 | tags = 'get_tags_list' 25 | -------------------------------------------------------------------------------- /backend/products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-11 21:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Product', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=120)), 19 | ('content', models.TextField(blank=True, null=True)), 20 | ('price', models.DecimalField(decimal_places=2, default=99.99, max_digits=15)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/products/migrations/0002_product_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-18 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 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('products', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='product', 18 | name='user', 19 | field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/products/migrations/0003_product_public.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-18 17:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0002_product_user'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='public', 16 | field=models.BooleanField(default=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/products/migrations/__init__.py -------------------------------------------------------------------------------- /backend/products/models.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.conf import settings 3 | from django.db import models 4 | from django.db.models import Q 5 | 6 | User = settings.AUTH_USER_MODEL # auth.User 7 | 8 | TAGS_MODEL_VALUES = ['electronics', 'cars', 'boats', 'movies', 'cameras'] 9 | 10 | class ProductQuerySet(models.QuerySet): 11 | def is_public(self): 12 | return self.filter(public=True) 13 | 14 | def search(self, query, user=None): 15 | lookup = Q(title__icontains=query) | Q(content__icontains=query) 16 | qs = self.is_public().filter(lookup) 17 | if user is not None: 18 | qs2 = self.filter(user=user).filter(lookup) 19 | qs = (qs | qs2).distinct() 20 | return qs 21 | 22 | class ProductManager(models.Manager): 23 | def get_queryset(self, *args,**kwargs): 24 | return ProductQuerySet(self.model, using=self._db) 25 | 26 | def search(self, query, user=None): 27 | return self.get_queryset().search(query, user=user) 28 | 29 | class Product(models.Model): 30 | # pk 31 | user = models.ForeignKey(User, default=1, null=True, on_delete=models.SET_NULL) 32 | title = models.CharField(max_length=120) 33 | content = models.TextField(blank=True, null=True) 34 | price = models.DecimalField(max_digits=15, decimal_places=2, default=99.99) 35 | public = models.BooleanField(default=True) 36 | 37 | objects = ProductManager() 38 | 39 | def get_absolute_url(self): 40 | return f"/api/products/{self.pk}/" 41 | 42 | @property 43 | def endpoint(self): 44 | return self.get_absolute_url() 45 | 46 | @property 47 | def path(self): 48 | return f"/products/{self.pk}/" 49 | 50 | @property 51 | def body(self): 52 | return self.content 53 | 54 | def is_public(self) -> bool: 55 | return self.public # True or False 56 | 57 | def get_tags_list(self): 58 | return [random.choice(TAGS_MODEL_VALUES)] 59 | 60 | @property 61 | def sale_price(self): 62 | return "%.2f" %(float(self.price) * 0.8) 63 | 64 | def get_discount(self): 65 | return "122" -------------------------------------------------------------------------------- /backend/products/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.reverse import reverse 3 | 4 | from api.serializers import UserPublicSerializer 5 | 6 | from .models import Product 7 | from . import validators 8 | 9 | 10 | class ProductInlineSerializer(serializers.Serializer): 11 | url = serializers.HyperlinkedIdentityField( 12 | view_name='product-detail', 13 | lookup_field='pk', 14 | read_only=True 15 | ) 16 | title = serializers.CharField(read_only=True) 17 | 18 | 19 | class ProductSerializer(serializers.ModelSerializer): 20 | owner = UserPublicSerializer(source='user', read_only=True) 21 | 22 | title = serializers.CharField(validators=[validators.validate_title_no_hello, validators.unique_product_title]) 23 | body = serializers.CharField(source='content') 24 | class Meta: 25 | model = Product 26 | fields = [ 27 | 'owner', 28 | 'pk', 29 | 'title', 30 | 'body', 31 | 'price', 32 | 'sale_price', 33 | 'public', 34 | 'path', 35 | 'endpoint', 36 | ] 37 | def get_my_user_data(self, obj): 38 | return { 39 | "username": obj.user.username 40 | } 41 | 42 | def get_edit_url(self, obj): 43 | request = self.context.get('request') # self.request 44 | if request is None: 45 | return None 46 | return reverse("product-edit", kwargs={"pk": obj.pk}, request=request) 47 | -------------------------------------------------------------------------------- /backend/products/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/products/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | # /api/products/ 6 | urlpatterns = [ 7 | path('', views.product_list_create_view, name='product-list'), 8 | path('/update/', views.product_update_view, name='product-edit'), 9 | path('/delete/', views.product_destroy_view), 10 | path('/', views.product_detail_view, name='product-detail') 11 | ] -------------------------------------------------------------------------------- /backend/products/validators.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.validators import UniqueValidator 3 | from .models import Product 4 | 5 | # def validate_title(value): 6 | # qs = Product.objects.filter(title__iexact=value) 7 | # if qs.exists(): 8 | # raise serializers.ValidationError(f"{value} is already a product name.") 9 | # return value 10 | 11 | def validate_title_no_hello(value): 12 | if "hello" in value.lower(): 13 | raise serializers.ValidationError(f"{value} is not allowed") 14 | return value 15 | 16 | 17 | unique_product_title = UniqueValidator(queryset=Product.objects.all(), lookup='iexact') -------------------------------------------------------------------------------- /backend/products/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics, mixins 2 | from rest_framework.decorators import api_view 3 | from rest_framework.response import Response 4 | # from django.http import Http404 5 | from django.shortcuts import get_object_or_404 6 | from api.mixins import ( 7 | StaffEditorPermissionMixin, 8 | UserQuerySetMixin) 9 | 10 | from .models import Product 11 | from .serializers import ProductSerializer 12 | 13 | class ProductListCreateAPIView( 14 | UserQuerySetMixin, 15 | StaffEditorPermissionMixin, 16 | generics.ListCreateAPIView): 17 | queryset = Product.objects.all() 18 | serializer_class = ProductSerializer 19 | 20 | def perform_create(self, serializer): 21 | # serializer.save(user=self.request.user) 22 | title = serializer.validated_data.get('title') 23 | content = serializer.validated_data.get('content') or None 24 | if content is None: 25 | content = title 26 | serializer.save(user=self.request.user, content=content) 27 | # send a Django signal 28 | 29 | # def get_queryset(self, *args, **kwargs): 30 | # qs = super().get_queryset(*args, **kwargs) 31 | # request = self.request 32 | # user = request.user 33 | # if not user.is_authenticated: 34 | # return Product.objects.none() 35 | # # print(request.user) 36 | # return qs.filter(user=request.user) 37 | 38 | 39 | product_list_create_view = ProductListCreateAPIView.as_view() 40 | 41 | class ProductDetailAPIView( 42 | UserQuerySetMixin, 43 | StaffEditorPermissionMixin, 44 | generics.RetrieveAPIView): 45 | queryset = Product.objects.all() 46 | serializer_class = ProductSerializer 47 | # lookup_field = 'pk' ?? 48 | 49 | product_detail_view = ProductDetailAPIView.as_view() 50 | 51 | 52 | class ProductUpdateAPIView( 53 | UserQuerySetMixin, 54 | StaffEditorPermissionMixin, 55 | generics.UpdateAPIView): 56 | queryset = Product.objects.all() 57 | serializer_class = ProductSerializer 58 | lookup_field = 'pk' 59 | 60 | def perform_update(self, serializer): 61 | instance = serializer.save() 62 | if not instance.content: 63 | instance.content = instance.title 64 | ## 65 | 66 | product_update_view = ProductUpdateAPIView.as_view() 67 | 68 | 69 | class ProductDestroyAPIView( 70 | UserQuerySetMixin, 71 | StaffEditorPermissionMixin, 72 | generics.DestroyAPIView): 73 | queryset = Product.objects.all() 74 | serializer_class = ProductSerializer 75 | lookup_field = 'pk' 76 | 77 | def perform_destroy(self, instance): 78 | # instance 79 | super().perform_destroy(instance) 80 | 81 | product_destroy_view = ProductDestroyAPIView.as_view() 82 | 83 | # class ProductListAPIView(generics.ListAPIView): 84 | # ''' 85 | # Not gonna use this method 86 | # ''' 87 | # queryset = Product.objects.all() 88 | # serializer_class = ProductSerializer 89 | 90 | # product_list_view = ProductListAPIView.as_view() 91 | 92 | 93 | class ProductMixinView( 94 | mixins.CreateModelMixin, 95 | mixins.ListModelMixin, 96 | mixins.RetrieveModelMixin, 97 | generics.GenericAPIView 98 | ): 99 | queryset = Product.objects.all() 100 | serializer_class = ProductSerializer 101 | lookup_field = 'pk' 102 | 103 | def get(self, request, *args, **kwargs): #HTTP -> get 104 | pk = kwargs.get("pk") 105 | if pk is not None: 106 | return self.retrieve(request, *args, **kwargs) 107 | return self.list(request, *args, **kwargs) 108 | 109 | def post(self, request, *args, **kwargs): 110 | return self.create(request, *args, **kwargs) 111 | 112 | def perform_create(self, serializer): 113 | # serializer.save(user=self.request.user) 114 | title = serializer.validated_data.get('title') 115 | content = serializer.validated_data.get('content') or None 116 | if content is None: 117 | content = "this is a single view doing cool stuff" 118 | serializer.save(content=content) 119 | 120 | # def post(): #HTTP -> post 121 | 122 | product_mixin_view = ProductMixinView.as_view() 123 | 124 | @api_view(['GET', 'POST']) 125 | def product_alt_view(request, pk=None, *args, **kwargs): 126 | method = request.method 127 | 128 | if method == "GET": 129 | if pk is not None: 130 | # detail view 131 | obj = get_object_or_404(Product, pk=pk) 132 | data = ProductSerializer(obj, many=False).data 133 | return Response(data) 134 | # list view 135 | queryset = Product.objects.all() 136 | data = ProductSerializer(queryset, many=True).data 137 | return Response(data) 138 | 139 | if method == "POST": 140 | # create an item 141 | serializer = ProductSerializer(data=request.data) 142 | if serializer.is_valid(raise_exception=True): 143 | title = serializer.validated_data.get('title') 144 | content = serializer.validated_data.get('content') or None 145 | if content is None: 146 | content = title 147 | serializer.save(content=content) 148 | return Response(serializer.data) 149 | return Response({"invalid": "not good data"}, status=400) 150 | -------------------------------------------------------------------------------- /backend/products/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import mixins, viewsets 2 | 3 | from .models import Product 4 | from .serializers import ProductSerializer 5 | 6 | class ProductViewSet(viewsets.ModelViewSet): 7 | ''' 8 | get -> list -> Queryset 9 | get -> retrieve -> Product Instance Detail View 10 | post -> create -> New Instance 11 | put -> Update 12 | patch -> Partial UPdate 13 | delete -> destroy 14 | ''' 15 | queryset = Product.objects.all() 16 | serializer_class = ProductSerializer 17 | lookup_field = 'pk' # default 18 | 19 | 20 | class ProductGenericViewSet( 21 | mixins.ListModelMixin, 22 | mixins.RetrieveModelMixin, 23 | viewsets.GenericViewSet): 24 | ''' 25 | get -> list -> Queryset 26 | get -> retrieve -> Product Instance Detail View 27 | ''' 28 | queryset = Product.objects.all() 29 | serializer_class = ProductSerializer 30 | lookup_field = 'pk' # default 31 | 32 | # product_list_view = ProductGenericViewSet.as_view({'get': 'list'}) 33 | # product_detail_view = ProductGenericViewSet.as_view({'get': 'retrieve'}) -------------------------------------------------------------------------------- /backend/search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/search/__init__.py -------------------------------------------------------------------------------- /backend/search/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/search/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SearchConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'search' 7 | -------------------------------------------------------------------------------- /backend/search/client.py: -------------------------------------------------------------------------------- 1 | from algoliasearch_django import algolia_engine 2 | 3 | 4 | def get_client(): 5 | return algolia_engine.client 6 | 7 | def get_index(index_name='cfe_Product'): 8 | # cfe_Article 9 | client = get_client() 10 | index = client.init_index(index_name) 11 | return index 12 | 13 | 14 | def perform_search(query, **kwargs): 15 | """ 16 | perform_search("hello", tags=["electronics"], public=True) 17 | """ 18 | index = get_index() 19 | params = {} 20 | tags = "" 21 | if "tags" in kwargs: 22 | tags = kwargs.pop("tags") or [] 23 | if len(tags) != 0: 24 | params['tagFilters'] = tags 25 | index_filters = [f"{k}:{v}" for k,v in kwargs.items() if v] 26 | if len(index_filters) != 0: 27 | params['facetFilters'] = index_filters 28 | print(params) 29 | results = index.search(query, params) 30 | return results -------------------------------------------------------------------------------- /backend/search/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Rest-Framework-Tutorial/2ee606fb8b6053134d686a4b0dffe1374d778c7c/backend/search/migrations/__init__.py -------------------------------------------------------------------------------- /backend/search/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/search/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/search/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.SearchListView.as_view(), name='search') 7 | ] -------------------------------------------------------------------------------- /backend/search/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from rest_framework.response import Response 3 | 4 | from products.models import Product 5 | from products.serializers import ProductSerializer 6 | 7 | from . import client 8 | 9 | class SearchListView(generics.GenericAPIView): 10 | def get(self, request, *args, **kwargs): 11 | user = None 12 | if request.user.is_authenticated: 13 | user = request.user.username 14 | query = request.GET.get('q') 15 | public = str(request.GET.get('public')) != "0" 16 | tag = request.GET.get('tag') or None 17 | if not query: 18 | return Response('', status=400) 19 | results = client.perform_search(query, tags=tag, user=user, public=public) 20 | return Response(results) 21 | 22 | class SearchListOldView(generics.ListAPIView): 23 | queryset = Product.objects.all() 24 | serializer_class = ProductSerializer 25 | 26 | def get_queryset(self, *args, **kwargs): 27 | qs = super().get_queryset(*args, **kwargs) 28 | q = self.request.GET.get('q') 29 | results = Product.objects.none() 30 | if q is not None: 31 | user = None 32 | if self.request.user.is_authenticated: 33 | user = self.request.user 34 | results = qs.search(q, user=user) 35 | return results -------------------------------------------------------------------------------- /drf.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /js_client/client.js: -------------------------------------------------------------------------------- 1 | const contentContainer = document.getElementById('content-container') 2 | const loginForm = document.getElementById('login-form') 3 | const searchForm = document.getElementById('search-form') 4 | const baseEndpoint = "http://localhost:8000/api" 5 | if (loginForm) { 6 | // handle this login form 7 | loginForm.addEventListener('submit', handleLogin) 8 | } 9 | if (searchForm) { 10 | // handle this login form 11 | searchForm.addEventListener('submit', handleSearch) 12 | } 13 | 14 | function handleLogin(event) { 15 | event.preventDefault() 16 | const loginEndpoint = `${baseEndpoint}/token/` 17 | let loginFormData = new FormData(loginForm) 18 | let loginObjectData = Object.fromEntries(loginFormData) 19 | let bodyStr = JSON.stringify(loginObjectData) 20 | const options = { 21 | method: "POST", 22 | headers: { 23 | "Content-Type": "application/json" 24 | }, 25 | body: bodyStr 26 | } 27 | fetch(loginEndpoint, options) // Promise 28 | .then(response=>{ 29 | return response.json() 30 | }) 31 | .then(authData => { 32 | handleAuthData(authData, getProductList) 33 | }) 34 | .catch(err=> { 35 | console.log('err', err) 36 | }) 37 | } 38 | 39 | function handleSearch(event) { 40 | event.preventDefault() 41 | let formData = new FormData(searchForm) 42 | let data = Object.fromEntries(formData) 43 | let searchParams = new URLSearchParams(data) 44 | const endpoint = `${baseEndpoint}/search/?${searchParams}` 45 | const headers = { 46 | "Content-Type": "application/json", 47 | } 48 | const authToken = localStorage.getItem('access') 49 | if (authToken) { 50 | headers['Authorization'] = `Bearer ${authToken}` 51 | } 52 | const options = { 53 | method: "GET", 54 | headers: headers 55 | } 56 | fetch(endpoint, options) // Promise 57 | .then(response=>{ 58 | return response.json() 59 | }) 60 | .then(data => { 61 | const validData = isTokenNotValid(data) 62 | if (validData && contentContainer){ 63 | contentContainer.innerHTML = "" 64 | if (data && data.hits) { 65 | let htmlStr = "" 66 | for (let result of data.hits) { 67 | htmlStr += "
  • "+ result.title + "
  • " 68 | } 69 | contentContainer.innerHTML = htmlStr 70 | if (data.hits.length === 0) { 71 | contentContainer.innerHTML = "

    No results found

    " 72 | } 73 | } else { 74 | contentContainer.innerHTML = "

    No results found

    " 75 | } 76 | } 77 | }) 78 | .catch(err=> { 79 | console.log('err', err) 80 | }) 81 | } 82 | 83 | function handleAuthData(authData, callback) { 84 | localStorage.setItem('access', authData.access) 85 | localStorage.setItem('refresh', authData.refresh) 86 | if (callback) { 87 | callback() 88 | } 89 | } 90 | 91 | function writeToContainer(data) { 92 | if (contentContainer) { 93 | contentContainer.innerHTML = "
    " + JSON.stringify(data, null, 4) + "
    " 94 | } 95 | } 96 | 97 | function getFetchOptions(method, body){ 98 | return { 99 | method: method === null ? "GET" : method, 100 | headers: { 101 | "Content-Type": "application/json", 102 | "Authorization": `Bearer ${localStorage.getItem('access')}` 103 | }, 104 | body: body ? body : null 105 | } 106 | } 107 | 108 | function isTokenNotValid(jsonData) { 109 | if (jsonData.code && jsonData.code === "token_not_valid"){ 110 | // run a refresh token fetch 111 | alert("Please login again") 112 | return false 113 | } 114 | return true 115 | } 116 | 117 | function validateJWTToken() { 118 | // fetch 119 | const endpoint = `${baseEndpoint}/token/verify/` 120 | const options = { 121 | method: "POST", 122 | headers: { 123 | "Content-Type": "application/json" 124 | }, 125 | body: JSON.stringify({ 126 | token: localStorage.getItem('access') 127 | }) 128 | } 129 | fetch(endpoint, options) 130 | .then(response=>response.json()) 131 | .then(x=> { 132 | // refresh token 133 | }) 134 | } 135 | 136 | function getProductList(){ 137 | const endpoint = `${baseEndpoint}/products/` 138 | const options = getFetchOptions() 139 | fetch(endpoint, options) 140 | .then(response=>{ 141 | return response.json() 142 | }) 143 | .then(data=> { 144 | const validData = isTokenNotValid(data) 145 | if (validData) { 146 | writeToContainer(data) 147 | } 148 | 149 | }) 150 | } 151 | 152 | validateJWTToken() 153 | // getProductList() 154 | 155 | 156 | const searchClient = algoliasearch('4IHLYNCMBJ', '2d98a3c1e68d4f81bbba206ca075cfbb'); 157 | 158 | const search = instantsearch({ 159 | indexName: 'cfe_Product', 160 | searchClient, 161 | }); 162 | 163 | search.addWidgets([ 164 | instantsearch.widgets.searchBox({ 165 | container: '#searchbox', 166 | }), 167 | 168 | instantsearch.widgets.clearRefinements({ 169 | container: "#clear-refinements" 170 | }), 171 | 172 | 173 | instantsearch.widgets.refinementList({ 174 | container: "#user-list", 175 | attribute: 'user' 176 | }), 177 | instantsearch.widgets.refinementList({ 178 | container: "#public-list", 179 | attribute: 'public' 180 | }), 181 | 182 | 183 | instantsearch.widgets.hits({ 184 | container: '#hits', 185 | templates: { 186 | item: ` 187 |
    188 |
    {{#helpers.highlight}}{ "attribute": "title" }{{/helpers.highlight}}
    189 |
    {{#helpers.highlight}}{ "attribute": "body" }{{/helpers.highlight}}
    190 | 191 |

    {{ user }}

    \${{ price }} 192 | 193 | 194 |

    ` 195 | } 196 | }) 197 | ]); 198 | 199 | search.start(); 200 | -------------------------------------------------------------------------------- /js_client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | 10 | 11 | 12 |
    13 | 14 | 18 | 19 |
    20 | 21 | 22 | 23 |
    24 |
    25 |

    Public

    26 |
    27 |
    28 | 29 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /py_client/basic.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | # endpoint = "https://httpbin.org/status/200/" 4 | # endpoint = "https://httpbin.org/anything" 5 | endpoint = "http://localhost:8000/api/" #http://127.0.0.1:8000/ 6 | 7 | get_response = requests.post(endpoint, json={"title": "Abc123", "content": "Hello world", "price": "abc134"}) # HTTP Request 8 | # print(get_response.headers) 9 | # print(get_response.text) # print raw text response 10 | # print(get_response.status_code) 11 | 12 | # HTTP Request -> HTML 13 | # REST API HTTP Request -> JSON 14 | # JavaScript Object Nototion ~ Python Dict 15 | print(get_response.json()) 16 | # print(get_response.status_code) -------------------------------------------------------------------------------- /py_client/create.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | headers = {'Authorization': 'Bearer 56be721aeecb0ed611e2765e9253e2d62f0badfc'} 5 | endpoint = "http://localhost:8000/api/products/" 6 | 7 | data = { 8 | "title": "This field is done", 9 | "price": 32.99 10 | } 11 | get_response = requests.post(endpoint, json=data, headers=headers) 12 | print(get_response.json()) -------------------------------------------------------------------------------- /py_client/delete.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | product_id = input("WHat is the product id you want to use?\n") 4 | try: 5 | product_id = int(product_id) 6 | except: 7 | product_id = None 8 | print(f'{product_id} not a valid id') 9 | 10 | if product_id: 11 | endpoint = f"http://localhost:8000/api/products/{product_id}/delete/" 12 | 13 | get_response = requests.delete(endpoint) 14 | print(get_response.status_code,get_response.status_code==204 ) -------------------------------------------------------------------------------- /py_client/detail.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | endpoint = "http://localhost:8000/api/products/1/" 4 | 5 | get_response = requests.get(endpoint) 6 | print(get_response.json()) -------------------------------------------------------------------------------- /py_client/jwt.py: -------------------------------------------------------------------------------- 1 | # Blog post reference: https://www.codingforentrepreneurs.com/blog/python-jwt-client-django-rest-framework-simplejwt 2 | 3 | from dataclasses import dataclass 4 | import requests 5 | from getpass import getpass 6 | import pathlib 7 | import json 8 | 9 | 10 | @dataclass 11 | class JWTClient: 12 | """ 13 | Use a dataclass decorator 14 | to simply the class construction 15 | """ 16 | access:str = None 17 | refresh:str = None 18 | # ensure this matches your simplejwt config 19 | header_type: str = "Bearer" 20 | # this assumesy ou have DRF running on localhost:8000 21 | base_endpoint = "http://localhost:8000/api" 22 | # this file path is insecure 23 | cred_path: pathlib.Path = pathlib.Path("creds.json") 24 | 25 | def __post_init__(self): 26 | if self.cred_path.exists(): 27 | """ 28 | You have stored creds, 29 | let's verify them 30 | and refresh them. 31 | If that fails, 32 | restart login process. 33 | """ 34 | try: 35 | data = json.loads(self.cred_path.read_text()) 36 | except Exception: 37 | print("Assuming creds has been tampered with") 38 | data = None 39 | if data is None: 40 | """ 41 | Clear stored creds and 42 | Run login process 43 | """ 44 | self.clear_tokens() 45 | self.perform_auth() 46 | else: 47 | """ 48 | `creds.json` was not tampered with 49 | Verify token -> 50 | if necessary, Refresh token -> 51 | if necessary, Run login process 52 | """ 53 | self.access = data.get('access') 54 | self.refresh = data.get('refresh') 55 | token_verified = self.verify_token() 56 | if not token_verified: 57 | """ 58 | This can mean the token has expired 59 | or is invalid. Either way, attempt 60 | a refresh. 61 | """ 62 | refreshed = self.perform_refresh() 63 | if not refreshed: 64 | """ 65 | This means the token refresh 66 | also failed. Run login process 67 | """ 68 | print("invalid data, login again.") 69 | self.clear_tokens() 70 | self.perform_auth() 71 | else: 72 | """ 73 | Run login process 74 | """ 75 | self.perform_auth() 76 | 77 | def get_headers(self, header_type=None): 78 | """ 79 | Default headers for HTTP requests 80 | including the JWT token 81 | """ 82 | _type = header_type or self.header_type 83 | token = self.access 84 | if not token: 85 | return {} 86 | return { 87 | "Authorization": f"{_type} {token}" 88 | } 89 | 90 | def perform_auth(self): 91 | """ 92 | Simple way to perform authentication 93 | Without exposing password(s) during the 94 | collection process. 95 | """ 96 | endpoint = f"{self.base_endpoint}/token/" 97 | username = input("What is your username?\n") 98 | password = getpass("What is your password?\n") 99 | r = requests.post(endpoint, json={'username': username, 'password': password}) 100 | if r.status_code != 200: 101 | raise Exception(f"Access not granted: {r.text}") 102 | print('access granted') 103 | self.write_creds(r.json()) 104 | 105 | def write_creds(self, data:dict): 106 | """ 107 | Store credentials as a local file 108 | and update instance with correct 109 | data. 110 | """ 111 | if self.cred_path is not None: 112 | self.access = data.get('access') 113 | self.refresh = data.get('refresh') 114 | if self.access and self.refresh: 115 | self.cred_path.write_text(json.dumps(data)) 116 | 117 | def verify_token(self): 118 | """ 119 | Simple method for verifying your 120 | token data. This method only verifies 121 | your `access` token. A 200 HTTP status 122 | means success, anything else means failure. 123 | """ 124 | data = { 125 | "token": f"{self.access}" 126 | } 127 | endpoint = f"{self.base_endpoint}/token/verify/" 128 | r = requests.post(endpoint, json=data) 129 | return r.status_code == 200 130 | 131 | def clear_tokens(self): 132 | """ 133 | Remove any/all JWT token data 134 | from instance as well as stored 135 | creds file. 136 | """ 137 | self.access = None 138 | self.refresh = None 139 | if self.cred_path.exists(): 140 | self.cred_path.unlink() 141 | 142 | def perform_refresh(self): 143 | """ 144 | Refresh the access token by using the correct 145 | auth headers and the refresh token. 146 | """ 147 | print("Refreshing token.") 148 | headers = self.get_headers() 149 | data = { 150 | "refresh": f"{self.refresh}" 151 | } 152 | endpoint = f"{self.base_endpoint}/token/refresh/" 153 | r = requests.post(endpoint, json=data, headers=headers) 154 | if r.status_code != 200: 155 | self.clear_tokens() 156 | return False 157 | refresh_data = r.json() 158 | if not 'access' in refresh_data: 159 | self.clear_tokens() 160 | return False 161 | stored_data = { 162 | 'access': refresh_data.get('access'), 163 | 'refresh': self.refresh 164 | } 165 | self.write_creds(stored_data) 166 | return True 167 | 168 | def list(self, endpoint=None, limit=3): 169 | """ 170 | Here is an actual api call to a DRF 171 | View that requires our simplejwt Authentication 172 | Working correctly. 173 | """ 174 | headers = self.get_headers() 175 | if endpoint is None or self.base_endpoint not in str(endpoint): 176 | endpoint = f"{self.base_endpoint}/products/?limit={limit}" 177 | r = requests.get(endpoint, headers=headers) 178 | if r.status_code != 200: 179 | raise Exception(f"Request not complete {r.text}") 180 | data = r.json() 181 | return data 182 | 183 | 184 | if __name__ == "__main__": 185 | """ 186 | Here's Simple example of how to use our client above. 187 | """ 188 | 189 | # this will either prompt a login process 190 | # or just run with current stored data 191 | client = JWTClient() 192 | 193 | # simple instance method to perform an HTTP 194 | # request to our /api/products/ endpoint 195 | lookup_1_data = client.list(limit=5) 196 | # We used pagination at our endpoint so we have: 197 | results = lookup_1_data.get('results') 198 | next_url = lookup_1_data.get('next') 199 | print("First lookup result length", len(results)) 200 | if next_url: 201 | lookup_2_data = client.list(endpoint=next_url) 202 | results += lookup_2_data.get('results') 203 | print("Second lookup result length", len(results)) -------------------------------------------------------------------------------- /py_client/list.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from getpass import getpass 3 | 4 | auth_endpoint = "http://localhost:8000/api/auth/" 5 | username = input("What is your username?\n") 6 | password = getpass("What is your password?\n") 7 | 8 | auth_response = requests.post(auth_endpoint, json={'username': username, 'password': password}) 9 | print(auth_response.json()) 10 | 11 | if auth_response.status_code == 200: 12 | token = auth_response.json()['token'] 13 | headers = { 14 | "Authorization": f"Bearer {token}" 15 | } 16 | endpoint = "http://localhost:8000/api/products/" 17 | 18 | get_response = requests.get(endpoint, headers=headers) 19 | 20 | data = get_response.json() 21 | next_url = data['next'] 22 | results = data['results'] 23 | print("next_url", next_url) 24 | print(results) 25 | # if next_url is not None: 26 | # get_response = requests.get(next_url, headers=headers) 27 | # print() -------------------------------------------------------------------------------- /py_client/not_found.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | endpoint = "http://localhost:8000/api/products/38098498139/" 4 | 5 | get_response = requests.get(endpoint) 6 | print(get_response.json()) -------------------------------------------------------------------------------- /py_client/update.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | endpoint = "http://localhost:8000/api/products/1/update/" 4 | 5 | data = { 6 | "title": "Hello world", 7 | "price": 0.00 8 | } 9 | 10 | get_response = requests.put(endpoint, json=data) 11 | print(get_response.json()) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | algoliasearch-django>=2.0,<3.0 2 | django>=4.0.0,<4.1.0 3 | djangorestframework 4 | djangorestframework-simplejwt 5 | pyyaml 6 | requests 7 | django-cors-headers --------------------------------------------------------------------------------