├── .gitignore
├── .gitpod.dockerfile
├── .gitpod.yml
├── .vscode
├── client.cnf
├── font_fix.py
├── heroku_config.sh
├── init_tasks.sh
├── launch.json
├── mysql.cnf
├── settings.json
├── since_update.sh
└── start_mysql.sh
├── Procfile
├── README.md
├── comments
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── serializers.py
├── tests.py
├── urls.py
└── views.py
├── drf_api
├── __init__.py
├── asgi.py
├── permissions.py
├── serializers.py
├── settings.py
├── urls.py
├── views.py
└── wsgi.py
├── followers
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── serializers.py
├── tests.py
├── urls.py
└── views.py
├── likes
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── serializers.py
├── tests.py
├── urls.py
└── views.py
├── manage.py
├── posts
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── serializers.py
├── tests.py
├── urls.py
└── views.py
├── profiles
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── serializers.py
├── tests.py
├── urls.py
└── views.py
├── requirements.txt
└── runtime.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | core.Microsoft*
2 | core.mongo*
3 | core.python*
4 | env.py
5 | __pycache__/
6 | *.py[cod]
7 | node_modules/
8 |
9 | # ignoring migrations
10 | **/migrations/**
11 | !**/migrations
12 | !**/migrations/__init__.py
13 | db.sqlite3
--------------------------------------------------------------------------------
/.gitpod.dockerfile:
--------------------------------------------------------------------------------
1 | FROM gitpod/workspace-base
2 |
3 | RUN echo "CI version from base"
4 |
5 | ### NodeJS ###
6 | USER gitpod
7 | ENV NODE_VERSION=16.13.0
8 | ENV TRIGGER_REBUILD=1
9 | RUN curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | PROFILE=/dev/null bash \
10 | && bash -c ". .nvm/nvm.sh \
11 | && nvm install $NODE_VERSION \
12 | && nvm alias default $NODE_VERSION \
13 | && npm install -g typescript yarn node-gyp" \
14 | && echo ". ~/.nvm/nvm.sh" >> /home/gitpod/.bashrc.d/50-node
15 | ENV PATH=$PATH:/home/gitpod/.nvm/versions/node/v${NODE_VERSION}/bin
16 |
17 | ### Python ###
18 | USER gitpod
19 | RUN sudo install-packages python3-pip
20 | ENV PYTHON_VERSION 3.9.17
21 |
22 | ENV PATH=$HOME/.pyenv/bin:$HOME/.pyenv/shims:$PATH
23 | RUN curl -fsSL https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash \
24 | && { echo; \
25 | echo 'eval "$(pyenv init -)"'; \
26 | echo 'eval "$(pyenv virtualenv-init -)"'; } >> /home/gitpod/.bashrc.d/60-python \
27 | && pyenv update \
28 | && pyenv install $PYTHON_VERSION \
29 | && pyenv global $PYTHON_VERSION \
30 | && python3 -m pip install --no-cache-dir --upgrade pip \
31 | && python3 -m pip install --no-cache-dir --upgrade \
32 | setuptools wheel virtualenv pipenv pylint rope flake8 \
33 | mypy autopep8 pep8 pylama pydocstyle bandit notebook \
34 | twine \
35 | && sudo rm -rf /tmp/*USER gitpod
36 | ENV PYTHONUSERBASE=/workspace/.pip-modules \
37 | PIP_USER=yes
38 | ENV PATH=$PYTHONUSERBASE/bin:$PATH
39 |
40 | # Setup Heroku CLI
41 | RUN curl https://cli-assets.heroku.com/install.sh | sh
42 |
43 | # Setup MongoDB (4.4 from Focal repos)
44 | RUN wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb && sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb && \
45 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 20691eec35216c63caf66ce1656408e390cfb1f5 && \
46 | sudo sh -c 'echo "deb http://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list' && \
47 | sudo apt-get update -y && \
48 | sudo touch /etc/init.d/mongod && \
49 | sudo apt-get install -y mongodb-org-shell && \
50 | sudo apt-get install -y links && \
51 | sudo apt-get clean -y && \
52 | sudo rm -rf /var/cache/apt/* /var/lib/apt/lists/* /tmp/* /home/gitpod/*.deb && \
53 | sudo chown -R gitpod:gitpod /home/gitpod/.cache/heroku/
54 |
55 | # Setup PostgreSQL
56 |
57 | RUN sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list' && \
58 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 && \
59 | sudo apt-get update -y && \
60 | sudo apt-get install -y postgresql-12
61 |
62 | ENV PGDATA="/workspace/.pgsql/data"
63 |
64 | RUN mkdir -p ~/.pg_ctl/bin ~/.pg_ctl/sockets \
65 | && echo '#!/bin/bash\n[ ! -d $PGDATA ] && mkdir -p $PGDATA && initdb --auth=trust -D $PGDATA\npg_ctl -D $PGDATA -l ~/.pg_ctl/log -o "-k ~/.pg_ctl/sockets" start\n' > ~/.pg_ctl/bin/pg_start \
66 | && echo '#!/bin/bash\npg_ctl -D $PGDATA -l ~/.pg_ctl/log -o "-k ~/.pg_ctl/sockets" stop\n' > ~/.pg_ctl/bin/pg_stop \
67 | && chmod +x ~/.pg_ctl/bin/*
68 |
69 | # ENV DATABASE_URL="postgresql://gitpod@localhost"
70 | # ENV PGHOSTADDR="127.0.0.1"
71 | ENV PGDATABASE="postgres"
72 |
73 | ENV PATH="/usr/lib/postgresql/12/bin:/home/gitpod/.nvm/versions/node/v${NODE_VERSION}/bin:$HOME/.pg_ctl/bin:$PATH"
74 |
75 |
76 | # Add aliases
77 |
78 | RUN echo 'alias run="python3 $GITPOD_REPO_ROOT/manage.py runserver 0.0.0.0:8000"' >> ~/.bashrc && \
79 | echo 'alias heroku_config=". $GITPOD_REPO_ROOT/.vscode/heroku_config.sh"' >> ~/.bashrc && \
80 | echo 'alias python=python3' >> ~/.bashrc && \
81 | echo 'alias pip=pip3' >> ~/.bashrc && \
82 | echo 'alias arctictern="python3 $GITPOD_REPO_ROOT/.vscode/arctictern.py"' >> ~/.bashrc && \
83 | echo 'alias font_fix="python3 $GITPOD_REPO_ROOT/.vscode/font_fix.py"' >> ~/.bashrc && \
84 | echo 'alias set_pg="export PGHOSTADDR=127.0.0.1"' >> ~/.bashrc && \
85 | echo 'alias mongosh=mongo' >> ~/.bashrc && \
86 | echo 'alias make_url="python3 $GITPOD_REPO_ROOT/.vscode/make_url.py "' >> ~/.bashrc && \
87 | echo 'FILE="$GITPOD_REPO_ROOT/.vscode/post_upgrade.sh"' >> ~/.bashrc && \
88 | echo 'if [ -z "$POST_UPGRADE_RUN" ]; then' >> ~/.bashrc && \
89 | echo ' if [[ -f "$FILE" ]]; then' >> ~/.bashrc && \
90 | echo ' . "$GITPOD_REPO_ROOT/.vscode/post_upgrade.sh"' >> ~/.bashrc && \
91 | echo " fi" >> ~/.bashrc && \
92 | echo "fi" >> ~/.bashrc
93 |
94 | # Local environment variables
95 | # C9USER is temporary to allow the MySQL Gist to run
96 | ENV C9_USER="root"
97 | ENV PORT="8080"
98 | ENV IP="0.0.0.0"
99 | ENV C9_HOSTNAME="localhost"
100 | # Despite the scary name, this is just to allow React and DRF to run together on Gitpod
101 | ENV DANGEROUSLY_DISABLE_HOST_CHECK=true
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | image:
2 | file: .gitpod.dockerfile
3 | tasks:
4 | - init: . ${GITPOD_REPO_ROOT}/.vscode/init_tasks.sh
5 | command: /home/gitpod/.pg_ctl/bin/pg_start > /dev/null
6 | - command: . ${GITPOD_REPO_ROOT}/.vscode/uptime.sh &
7 | vscode:
8 | extensions:
9 | - ms-python.python
10 | - formulahendry.auto-close-tag
11 | - mkaufman.HTMLHint
12 | - eventyret.bootstrap-4-cdn-snippet
13 | - kevinglasson.cornflakes-linter
14 | - hookyqr.beautify
15 | - matt-rudge.auto-open-preview-panel
16 |
--------------------------------------------------------------------------------
/.vscode/client.cnf:
--------------------------------------------------------------------------------
1 | [client]
2 | host = localhost
3 | user = root
4 | password =
5 | socket = /var/run/mysqld/mysqld.sock
6 | [mysql_upgrade]
7 | host = localhost
8 | user = root
9 | password =
10 | socket = /var/run/mysqld/mysqld.sock
11 |
--------------------------------------------------------------------------------
/.vscode/font_fix.py:
--------------------------------------------------------------------------------
1 | # Fixes the font issue on Brave browser
2 | # Matt Rudge
3 | # June 2021
4 |
5 | import json
6 | import os
7 |
8 | BASE_PATH = os.environ.get("GITPOD_REPO_ROOT")
9 |
10 | with open(f"{BASE_PATH}/.vscode/settings.json", "r+") as f:
11 | content = json.loads(f.read())
12 |
13 | if "terminal.integrated.fontFamily" not in content:
14 | print("Terminal Font Fix: adding Menlo font")
15 | content["terminal.integrated.fontFamily"] = "Menlo"
16 | else:
17 | print("Terminal Font Fix: removing Menlo font")
18 | content.pop("terminal.integrated.fontFamily")
19 |
20 | f.seek(0, os.SEEK_SET)
21 | f.write(json.dumps(content))
22 | f.truncate()
23 |
--------------------------------------------------------------------------------
/.vscode/heroku_config.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to allow Heroku API key to be pasted
3 | # exported as an environment variable
4 | #
5 | # Matt Rudge, May 2021
6 |
7 | echo Heroku authentication configuration script
8 | echo Code Institute, 2021
9 | echo
10 | echo Get your Heroku API key by going to https://dashboard.heroku.com
11 | echo Go to Account Settings and click on Reveal to view your Heroku API key
12 | echo
13 |
14 | if [[ -z "${HEROKU_API_KEY}" ]]; then
15 | echo Paste your Heroku API key here or press Enter to quit:
16 | read apikey
17 | if [[ -z "${apikey}" ]]; then
18 | return 0
19 | fi
20 | echo export HEROKU_API_KEY=${apikey} >> ~/.bashrc
21 | echo Added the export. Refreshing the terminal.
22 | . ~/.bashrc > /dev/null
23 | echo Done!
24 | else
25 | echo API key is already set. Exiting
26 | fi
27 |
--------------------------------------------------------------------------------
/.vscode/init_tasks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Creates a user record for the current Cloud9 user
4 | # Gives a personalised greeting
5 | # Adds configuration options for SQLite
6 | # Creates run aliases
7 | # Author: Matt Rudge
8 |
9 | echo "Setting the greeting"
10 | sed -i "s/USER_NAME/$GITPOD_GIT_USER_NAME/g" ${GITPOD_REPO_ROOT}/README.md
11 | echo "Creating the ${C9_USER} user in MySQL"
12 | RESULT="$(mysql -sse "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '${C9_USER}')")"
13 | if [ "$RESULT" = 1 ]; then
14 | echo "${C9_USER} already exists"
15 | else
16 | mysql -e "CREATE USER '${C9_USER}'@'%' IDENTIFIED BY '';" -u root
17 | echo "Granting privileges"
18 | mysql -e "GRANT ALL PRIVILEGES ON *.* TO '${C9_USER}'@'%' WITH GRANT OPTION;" -u root
19 | fi
20 | echo "Creating .sqliterc file"
21 | echo ".headers on" > ~/.sqliterc
22 | echo ".mode column" >> ~/.sqliterc
23 | source ~/.bashrc
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | "version": "0.2.0",
5 | "configurations": [
6 | {
7 | "name": "Python: Current File (Integrated Terminal)",
8 | "type": "python",
9 | "request": "launch",
10 | "program": "${file}",
11 | "console": "internalConsole"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/mysql.cnf:
--------------------------------------------------------------------------------
1 | [mysqld_safe]
2 | socket = /var/run/mysqld/mysqld.sock
3 | nice = 0
4 |
5 | [mysqld]
6 | user = gitpod
7 | pid-file = /var/run/mysqld/mysqld.pid
8 | socket = /var/run/mysqld/mysqld.sock
9 | port = 3306
10 | basedir = /usr
11 | datadir = /workspace/mysql
12 | tmpdir = /tmp
13 | lc-messages-dir = /usr/share/mysql
14 | skip-external-locking
15 |
16 | key_buffer_size = 16M
17 | max_allowed_packet = 16M
18 | thread_stack = 192K
19 | thread_cache_size = 8
20 |
21 | myisam-recover-options = BACKUP
22 |
23 | general_log_file = /var/log/mysql/mysql.log
24 | general_log = 1
25 | log_error = /var/log/mysql/error.log
26 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.linting.pylintEnabled": false,
3 | "python.linting.enabled": true,
4 | "python.linting.pycodestyleEnabled": true,
5 | "python.linting.flake8Enabled": false,
6 | "python.terminal.activateEnvironment": false,
7 | "python.formatting.autopep8Path": "/home/gitpod/.pyenv/shims/autopep8",
8 | "python.linting.flake8Path": "/home/gitpod/.pyenv/shims/flake8",
9 | "cornflakes.linter.executablePath": "/home/gitpod/.pyenv/shims/flake8",
10 | "files.exclude": {
11 | "**/.DS_Store": true,
12 | "**/.git": true,
13 | "**/.github": true,
14 | "**/.gitp*": true,
15 | "**/.hg": true,
16 | "**/.svn": true,
17 | "**/.vscode": true,
18 | "**/core.Microsoft*": true,
19 | "**/core.mongo*": true,
20 | "**/core.python*": true,
21 | "**/CVS": true
22 | },
23 | "files.autoSave": "off",
24 | "workbench.colorTheme": "Visual Studio Dark",
25 | "terminal.integrated.gpuAcceleration": "canvas",
26 | "editor.defaultFormatter": "HookyQR.beautify"
27 | }
--------------------------------------------------------------------------------
/.vscode/since_update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Post update script, add in changes to init_tasks.sh
3 | # that won't take effect in an upgraded workspace
4 |
5 | echo 'alias heroku_config=". $GITPOD_REPO_ROOT/.vscode/heroku_config.sh"' >> ~/.bashrc
6 |
7 | echo Post-upgrade changes applied
8 |
--------------------------------------------------------------------------------
/.vscode/start_mysql.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # this script is intended to be called from .bashrc
4 | # This is a workaround for not having something like supervisord
5 |
6 | if [ ! -e /var/run/mysqld/gitpod-init.lock ]
7 | then
8 | touch /var/run/mysqld/gitpod-init.lock
9 |
10 | # initialize database structures on disk, if needed
11 | [ ! -d /workspace/mysql ] && mysqld --initialize-insecure
12 |
13 | # launch database, if not running
14 | [ ! -e /var/run/mysqld/mysqld.pid ] && mysqld --daemonize
15 |
16 | rm /var/run/mysqld/gitpod-init.lock
17 | fi
18 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | release: python manage.py makemigrations && python manage.py migrate
2 | web: gunicorn drf_api.wsgi
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Moments - API
2 | ## Project description
3 | Moments is a social media platform. It has been designed for its users to share their life's moments. The application consists of the React app and an API. Welcome to the Django Rest Framework API project section.
4 |
5 | ## User stories
6 | | Category | as | I want to | so that I can | mapping API feature |
7 | | --------- | -------- | ------------------------------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------- |
8 | | auth | user | register for an account | have a personal profile with a picture | dj-rest-auth
Create profile (signals) |
9 | | auth | user | register for an account | create, like and comment on posts | Create post
Create comment
Create like |
10 | | auth | user | register for an account | follow users | Create follower |
11 | | posts | visitor | view a list of posts | browse the most recent uploads | List/ Filter posts |
12 | | posts | visitor | view an individual post | see user feedback, i.e. likes and read comments | Retrieve post |
13 | | posts | visitor | search a list of posts | find a post by a specific artist or a title | List/ Filter posts |
14 | | posts | visitor | scroll through a list of posts | browse the site more comfortably | List/ Filter posts |
15 | | posts | user | edit and delete my post | correct or hide any mistakes | Update property
Destroy property |
16 | | posts | user | create a post | share my moments with others | Create post |
17 | | posts | user | view liked posts | go back often to my favourite posts | List/ Filter posts |
18 | | posts | user | view followed users' posts | keep up with my favourite users' moments | List/ Filter posts |
19 | | likes | user | like a post | express my interest in someone's shared moment | Create like |
20 | | likes | user | unlike a post | express that my interest in someone's shared moment has faded away | Destroy like |
21 | | comments | user | create a comment | share my thoughts on other people's content | Create comment |
22 | | comments | user | edit and delete my comment | correct or hide any mistakes | Update comment
Destroy comment |
23 | | profiles | user | view a profile | see a user's recent posts + post, followers, following count data | Retrieve profile
List/ filter posts |
24 | | profiles | user | edit a profile | update my profile information | Update profile |
25 | | followers | user | follow a profile | express my interest in someone's content | Create follower |
26 | | followers | user | unfollow a profile | express that my interest in someone's content has faded away and remove their posts from my feed | Destroy follower |
27 |
28 | ## Entity Relationship Diagram
29 | 
30 |
31 | ## Models and CRUD breakdown
32 | | model | endpoints | create | retrieve | update | delete | filter | text search |
33 | | --------- | ---------------------------- | ------------- | -------- | ------ | ------ | ------------------------ | ----------- |
34 | | users | users/
users/:id/ | yes | yes | yes | no | no | no |
35 | | profiles | profiles/
profiles/:id/ | yes (signals) | yes | yes | no | following
followed | name |
36 | | likes | likes/
likes/:id/ | yes | yes | no | yes | no | no |
37 | | comments | comments/
comments/:id/ | yes | yes | yes | yes | post | no |
38 | | followers | followers/
followers/:id/ | yes | yes | no | yes | no | no |
39 | | posts | posts/
posts/:id/ | yes | yes | yes | yes | profile
liked
feed | title |
40 |
41 | ## Tests
42 | - Posts app:
43 | - logged out users can list posts
44 | - logged in users can create a post
45 | - logged out users can't create a post
46 | - logged out users can retrieve a post with a valid id
47 | - logged out users can't retrieve a post with an invalid id
48 | - logged in users can update a post they own
49 | - logged in users can't update a post they don't own
50 |
51 | ## Deployment steps
52 | - set the following environment variables:
53 | - CLIENT_ORIGIN
54 | - CLOUDINARY_URL
55 | - DATABASE_URL
56 | - DISABLE_COLLECTSTATIC
57 | - SECRET_KEY
58 | - installed the following libraries to handle database connection:
59 | - psycopg2
60 | - dj-database-url
61 | - configured dj-rest-auth library for JWTs
62 | - set allowed hosts
63 | - configured CORS:
64 | - set allowed_origins
65 | - set default renderer to JSON
66 | - added Procfile with release and web commands
67 | - gitignored the env.py file
68 | - generated requirements.txt
69 | - deployed to Heroku
70 |
71 | ---
72 |
73 | Happy coding!
74 |
--------------------------------------------------------------------------------
/comments/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/comments/__init__.py
--------------------------------------------------------------------------------
/comments/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/comments/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CommentsConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'comments'
7 |
--------------------------------------------------------------------------------
/comments/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/comments/migrations/__init__.py
--------------------------------------------------------------------------------
/comments/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from posts.models import Post
4 |
5 |
6 | class Comment(models.Model):
7 | """
8 | Comment model, related to User and Post
9 | """
10 | owner = models.ForeignKey(User, on_delete=models.CASCADE)
11 | post = models.ForeignKey(Post, on_delete=models.CASCADE)
12 | created_at = models.DateTimeField(auto_now_add=True)
13 | updated_at = models.DateTimeField(auto_now=True)
14 | content = models.TextField()
15 |
16 | class Meta:
17 | ordering = ['-created_at']
18 |
19 | def __str__(self):
20 | return self.content
21 |
--------------------------------------------------------------------------------
/comments/serializers.py:
--------------------------------------------------------------------------------
1 | from django.contrib.humanize.templatetags.humanize import naturaltime
2 | from rest_framework import serializers
3 | from .models import Comment
4 |
5 |
6 | class CommentSerializer(serializers.ModelSerializer):
7 | """
8 | Serializer for the Comment model
9 | Adds three extra fields when returning a list of Comment instances
10 | """
11 | owner = serializers.ReadOnlyField(source='owner.username')
12 | is_owner = serializers.SerializerMethodField()
13 | profile_id = serializers.ReadOnlyField(source='owner.profile.id')
14 | profile_image = serializers.ReadOnlyField(source='owner.profile.image.url')
15 | created_at = serializers.SerializerMethodField()
16 | updated_at = serializers.SerializerMethodField()
17 |
18 | def get_is_owner(self, obj):
19 | request = self.context['request']
20 | return request.user == obj.owner
21 |
22 | def get_created_at(self, obj):
23 | return naturaltime(obj.created_at)
24 |
25 | def get_updated_at(self, obj):
26 | return naturaltime(obj.updated_at)
27 |
28 | class Meta:
29 | model = Comment
30 | fields = [
31 | 'id', 'owner', 'is_owner', 'profile_id', 'profile_image',
32 | 'post', 'created_at', 'updated_at', 'content'
33 | ]
34 |
35 |
36 | class CommentDetailSerializer(CommentSerializer):
37 | """
38 | Serializer for the Comment model used in Detail view
39 | Post is a read only field so that we dont have to set it on each update
40 | """
41 | post = serializers.ReadOnlyField(source='post.id')
42 |
--------------------------------------------------------------------------------
/comments/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/comments/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from comments import views
3 |
4 | urlpatterns = [
5 | path('comments/', views.CommentList.as_view()),
6 | path('comments//', views.CommentDetail.as_view())
7 | ]
8 |
--------------------------------------------------------------------------------
/comments/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics, permissions
2 | from django_filters.rest_framework import DjangoFilterBackend
3 | from drf_api.permissions import IsOwnerOrReadOnly
4 | from .models import Comment
5 | from .serializers import CommentSerializer, CommentDetailSerializer
6 |
7 |
8 | class CommentList(generics.ListCreateAPIView):
9 | """
10 | List comments or create a comment if logged in.
11 | """
12 | serializer_class = CommentSerializer
13 | permission_classes = [permissions.IsAuthenticatedOrReadOnly]
14 | queryset = Comment.objects.all()
15 | filter_backends = [DjangoFilterBackend]
16 | filterset_fields = ['post']
17 |
18 | def perform_create(self, serializer):
19 | serializer.save(owner=self.request.user)
20 |
21 |
22 | class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
23 | """
24 | Retrieve a comment, or update or delete it by id if you own it.
25 | """
26 | permission_classes = [IsOwnerOrReadOnly]
27 | serializer_class = CommentDetailSerializer
28 | queryset = Comment.objects.all()
29 |
--------------------------------------------------------------------------------
/drf_api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/drf_api/__init__.py
--------------------------------------------------------------------------------
/drf_api/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for drf_api project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/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', 'drf_api.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/drf_api/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework import permissions
2 |
3 |
4 | class IsOwnerOrReadOnly(permissions.BasePermission):
5 | def has_object_permission(self, request, view, obj):
6 | if request.method in permissions.SAFE_METHODS:
7 | return True
8 | return obj.owner == request.user
9 |
--------------------------------------------------------------------------------
/drf_api/serializers.py:
--------------------------------------------------------------------------------
1 | from dj_rest_auth.serializers import UserDetailsSerializer
2 | from rest_framework import serializers
3 |
4 |
5 | class CurrentUserSerializer(UserDetailsSerializer):
6 | profile_id = serializers.ReadOnlyField(source='profile.id')
7 | profile_image = serializers.ReadOnlyField(source='profile.image.url')
8 |
9 | class Meta(UserDetailsSerializer.Meta):
10 | fields = UserDetailsSerializer.Meta.fields + (
11 | 'profile_id', 'profile_image'
12 | )
13 |
--------------------------------------------------------------------------------
/drf_api/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for drf_api project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.2.4.
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 |
13 | from pathlib import Path
14 | import os
15 | import re
16 | import dj_database_url
17 |
18 | if os.path.exists('env.py'):
19 | import env
20 |
21 | CLOUDINARY_STORAGE = {
22 | 'CLOUDINARY_URL': os.environ.get('CLOUDINARY_URL')
23 | }
24 | MEDIA_URL = '/media/'
25 | DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
26 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
27 | BASE_DIR = Path(__file__).resolve().parent.parent
28 |
29 | REST_FRAMEWORK = {
30 | 'DEFAULT_AUTHENTICATION_CLASSES': [(
31 | 'rest_framework.authentication.SessionAuthentication'
32 | if 'DEV' in os.environ
33 | else 'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
34 | )],
35 | 'DEFAULT_PAGINATION_CLASS':
36 | 'rest_framework.pagination.PageNumberPagination',
37 | 'PAGE_SIZE': 10,
38 | 'DATETIME_FORMAT': '%d %b %Y',
39 | }
40 | if 'DEV' not in os.environ:
41 | REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = [
42 | 'rest_framework.renderers.JSONRenderer',
43 | ]
44 |
45 | REST_USE_JWT = True
46 | JWT_AUTH_SECURE = True
47 | JWT_AUTH_COOKIE = 'my-app-auth'
48 | JWT_AUTH_REFRESH_COOKIE = 'my-refresh-token'
49 | JWT_AUTH_SAMESITE = 'None'
50 |
51 | REST_AUTH_SERIALIZERS = {
52 | 'USER_DETAILS_SERIALIZER': 'drf_api.serializers.CurrentUserSerializer'
53 | }
54 |
55 | # Quick-start development settings - unsuitable for production
56 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
57 |
58 | # SECURITY WARNING: keep the secret key used in production secret!
59 | SECRET_KEY = os.environ.get('SECRET_KEY')
60 |
61 | # SECURITY WARNING: don't run with debug turned on in production!
62 | DEBUG = 'DEV' in os.environ
63 |
64 | ALLOWED_HOSTS = [
65 | os.environ.get('ALLOWED_HOST'),
66 | 'localhost',
67 | ]
68 |
69 | if 'CLIENT_ORIGIN' in os.environ:
70 | CORS_ALLOWED_ORIGINS = [
71 | os.environ.get('CLIENT_ORIGIN')
72 | ]
73 |
74 | if 'CLIENT_ORIGIN_DEV' in os.environ:
75 | extracted_url = re.match(
76 | r'^.+-', os.environ.get('CLIENT_ORIGIN_DEV', ''), re.IGNORECASE
77 | ).group(0)
78 | CORS_ALLOWED_ORIGIN_REGEXES = [
79 | rf"{extracted_url}(eu|us)\d+\w\.gitpod\.io$",
80 | ]
81 |
82 | CORS_ALLOW_CREDENTIALS = True
83 |
84 | # Application definition
85 |
86 | INSTALLED_APPS = [
87 | 'django.contrib.admin',
88 | 'django.contrib.auth',
89 | 'django.contrib.contenttypes',
90 | 'django.contrib.sessions',
91 | 'django.contrib.messages',
92 | 'cloudinary_storage',
93 | 'django.contrib.staticfiles',
94 | 'cloudinary',
95 | 'rest_framework',
96 | 'django_filters',
97 | 'rest_framework.authtoken',
98 | 'dj_rest_auth',
99 | 'django.contrib.sites',
100 | 'allauth',
101 | 'allauth.account',
102 | 'allauth.socialaccount',
103 | 'dj_rest_auth.registration',
104 | 'corsheaders',
105 |
106 | 'profiles',
107 | 'posts',
108 | 'comments',
109 | 'likes',
110 | 'followers',
111 | ]
112 | SITE_ID = 1
113 | MIDDLEWARE = [
114 | 'corsheaders.middleware.CorsMiddleware',
115 | 'django.middleware.security.SecurityMiddleware',
116 | 'django.contrib.sessions.middleware.SessionMiddleware',
117 | 'django.middleware.common.CommonMiddleware',
118 | 'django.middleware.csrf.CsrfViewMiddleware',
119 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
120 | 'django.contrib.messages.middleware.MessageMiddleware',
121 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
122 | ]
123 |
124 | ROOT_URLCONF = 'drf_api.urls'
125 |
126 | TEMPLATES = [
127 | {
128 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
129 | 'DIRS': [],
130 | 'APP_DIRS': True,
131 | 'OPTIONS': {
132 | 'context_processors': [
133 | 'django.template.context_processors.debug',
134 | 'django.template.context_processors.request',
135 | 'django.contrib.auth.context_processors.auth',
136 | 'django.contrib.messages.context_processors.messages',
137 | ],
138 | },
139 | },
140 | ]
141 |
142 | WSGI_APPLICATION = 'drf_api.wsgi.application'
143 |
144 |
145 | # Database
146 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
147 |
148 | DATABASES = {
149 | 'default': ({
150 | 'ENGINE': 'django.db.backends.sqlite3',
151 | 'NAME': BASE_DIR / 'db.sqlite3',
152 | } if 'DEV' in os.environ else dj_database_url.parse(
153 | os.environ.get('DATABASE_URL')
154 | ))
155 | }
156 |
157 |
158 | # Password validation
159 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
160 |
161 | AUTH_PASSWORD_VALIDATORS = [
162 | {
163 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
164 | },
165 | {
166 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
167 | },
168 | {
169 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
170 | },
171 | {
172 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
173 | },
174 | ]
175 |
176 |
177 | # Internationalization
178 | # https://docs.djangoproject.com/en/3.2/topics/i18n/
179 |
180 | LANGUAGE_CODE = 'en-us'
181 |
182 | TIME_ZONE = 'UTC'
183 |
184 | USE_I18N = True
185 |
186 | USE_L10N = True
187 |
188 | USE_TZ = True
189 |
190 |
191 | # Static files (CSS, JavaScript, Images)
192 | # https://docs.djangoproject.com/en/3.2/howto/static-files/
193 |
194 | STATIC_URL = '/static/'
195 |
196 | # Default primary key field type
197 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
198 |
199 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
200 |
--------------------------------------------------------------------------------
/drf_api/urls.py:
--------------------------------------------------------------------------------
1 | """drf_api 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 | from .views import root_route, logout_route
19 |
20 | urlpatterns = [
21 | path('', root_route),
22 | path('admin/', admin.site.urls),
23 | path('api-auth/', include('rest_framework.urls')),
24 | # our logout route has to be above the default one to be matched first
25 | path('dj-rest-auth/logout/', logout_route),
26 | path('dj-rest-auth/', include('dj_rest_auth.urls')),
27 | path(
28 | 'dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')
29 | ),
30 | path('', include('profiles.urls')),
31 | path('', include('posts.urls')),
32 | path('', include('comments.urls')),
33 | path('', include('likes.urls')),
34 | path('', include('followers.urls')),
35 | ]
36 |
--------------------------------------------------------------------------------
/drf_api/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 | from .settings import (
4 | JWT_AUTH_COOKIE, JWT_AUTH_REFRESH_COOKIE, JWT_AUTH_SAMESITE,
5 | JWT_AUTH_SECURE,
6 | )
7 |
8 |
9 | @api_view()
10 | def root_route(request):
11 | return Response({
12 | "message": "Welcome to my drf API!"
13 | })
14 |
15 |
16 | # dj-rest-auth logout view fix
17 | @api_view(['POST'])
18 | def logout_route(request):
19 | response = Response()
20 | response.set_cookie(
21 | key=JWT_AUTH_COOKIE,
22 | value='',
23 | httponly=True,
24 | expires='Thu, 01 Jan 1970 00:00:00 GMT',
25 | max_age=0,
26 | samesite=JWT_AUTH_SAMESITE,
27 | secure=JWT_AUTH_SECURE,
28 | )
29 | response.set_cookie(
30 | key=JWT_AUTH_REFRESH_COOKIE,
31 | value='',
32 | httponly=True,
33 | expires='Thu, 01 Jan 1970 00:00:00 GMT',
34 | max_age=0,
35 | samesite=JWT_AUTH_SAMESITE,
36 | secure=JWT_AUTH_SECURE,
37 | )
38 | return response
39 |
--------------------------------------------------------------------------------
/drf_api/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for drf_api project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/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', 'drf_api.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/followers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/followers/__init__.py
--------------------------------------------------------------------------------
/followers/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/followers/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class FollowersConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'followers'
7 |
--------------------------------------------------------------------------------
/followers/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/followers/migrations/__init__.py
--------------------------------------------------------------------------------
/followers/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class Follower(models.Model):
6 | """
7 | Follower model, related to 'owner' and 'followed'.
8 | 'owner' is a User that is following a User.
9 | 'followed' is a User that is followed by 'owner'.
10 | We need the related_name attribute so that django can differentiate.
11 | between 'owner' and 'followed' who both are User model instances.
12 | 'unique_together' makes sure a user can't 'double follow' the same user.
13 | """
14 | owner = models.ForeignKey(
15 | User, related_name='following', on_delete=models.CASCADE
16 | )
17 | followed = models.ForeignKey(
18 | User, related_name='followed', on_delete=models.CASCADE
19 | )
20 | created_at = models.DateTimeField(auto_now_add=True)
21 |
22 | class Meta:
23 | ordering = ['-created_at']
24 | unique_together = ['owner', 'followed']
25 |
26 | def __str__(self):
27 | return f'{self.owner} {self.followed}'
28 |
--------------------------------------------------------------------------------
/followers/serializers.py:
--------------------------------------------------------------------------------
1 | from django.db import IntegrityError
2 | from rest_framework import serializers
3 | from .models import Follower
4 |
5 |
6 | class FollowerSerializer(serializers.ModelSerializer):
7 | """
8 | Serializer for the Follower model
9 | Create method handles the unique constraint on 'owner' and 'followed'
10 | """
11 | owner = serializers.ReadOnlyField(source='owner.username')
12 | followed_name = serializers.ReadOnlyField(source='followed.username')
13 |
14 | class Meta:
15 | model = Follower
16 | fields = [
17 | 'id', 'owner', 'created_at', 'followed', 'followed_name'
18 | ]
19 |
20 | def create(self, validated_data):
21 | try:
22 | return super().create(validated_data)
23 | except IntegrityError:
24 | raise serializers.ValidationError({'detail': 'possible duplicate'})
25 |
--------------------------------------------------------------------------------
/followers/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/followers/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from followers import views
3 |
4 | urlpatterns = [
5 | path('followers/', views.FollowerList.as_view()),
6 | path('followers//', views.FollowerDetail.as_view())
7 | ]
8 |
--------------------------------------------------------------------------------
/followers/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics, permissions
2 | from drf_api.permissions import IsOwnerOrReadOnly
3 | from .models import Follower
4 | from .serializers import FollowerSerializer
5 |
6 |
7 | class FollowerList(generics.ListCreateAPIView):
8 | """
9 | List all followers, i.e. all instances of a user
10 | following another user'.
11 | Create a follower, i.e. follow a user if logged in.
12 | Perform_create: associate the current logged in user with a follower.
13 | """
14 | permission_classes = [permissions.IsAuthenticatedOrReadOnly]
15 | queryset = Follower.objects.all()
16 | serializer_class = FollowerSerializer
17 |
18 | def perform_create(self, serializer):
19 | serializer.save(owner=self.request.user)
20 |
21 |
22 | class FollowerDetail(generics.RetrieveDestroyAPIView):
23 | """
24 | Retrieve a follower
25 | No Update view, as we either follow or unfollow users
26 | Destroy a follower, i.e. unfollow someone if owner
27 | """
28 | permission_classes = [IsOwnerOrReadOnly]
29 | queryset = Follower.objects.all()
30 | serializer_class = FollowerSerializer
31 |
--------------------------------------------------------------------------------
/likes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/likes/__init__.py
--------------------------------------------------------------------------------
/likes/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/likes/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class LikesConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'likes'
7 |
--------------------------------------------------------------------------------
/likes/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/likes/migrations/__init__.py
--------------------------------------------------------------------------------
/likes/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from posts.models import Post
4 |
5 |
6 | class Like(models.Model):
7 | """
8 | Like model, related to 'owner' and 'post'.
9 | 'owner' is a User instance and 'post' is a Post instance.
10 | 'unique_together' makes sure a user can't like the same post twice.
11 | """
12 | owner = models.ForeignKey(User, on_delete=models.CASCADE)
13 | post = models.ForeignKey(
14 | Post, related_name='likes', on_delete=models.CASCADE
15 | )
16 | created_at = models.DateTimeField(auto_now_add=True)
17 |
18 | class Meta:
19 | ordering = ['-created_at']
20 | unique_together = ['owner', 'post']
21 |
22 | def __str__(self):
23 | return f'{self.owner} {self.post}'
24 |
--------------------------------------------------------------------------------
/likes/serializers.py:
--------------------------------------------------------------------------------
1 | from django.db import IntegrityError
2 | from rest_framework import serializers
3 | from likes.models import Like
4 |
5 |
6 | class LikeSerializer(serializers.ModelSerializer):
7 | """
8 | Serializer for the Like model
9 | The create method handles the unique constraint on 'owner' and 'post'
10 | """
11 | owner = serializers.ReadOnlyField(source='owner.username')
12 |
13 | class Meta:
14 | model = Like
15 | fields = ['id', 'created_at', 'owner', 'post']
16 |
17 | def create(self, validated_data):
18 | try:
19 | return super().create(validated_data)
20 | except IntegrityError:
21 | raise serializers.ValidationError({
22 | 'detail': 'possible duplicate'
23 | })
24 |
--------------------------------------------------------------------------------
/likes/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/likes/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from likes import views
3 |
4 | urlpatterns = [
5 | path('likes/', views.LikeList.as_view()),
6 | path('likes//', views.LikeDetail.as_view()),
7 | ]
8 |
--------------------------------------------------------------------------------
/likes/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics, permissions
2 | from drf_api.permissions import IsOwnerOrReadOnly
3 | from likes.models import Like
4 | from likes.serializers import LikeSerializer
5 |
6 |
7 | class LikeList(generics.ListCreateAPIView):
8 | """
9 | List likes or create a like if logged in.
10 | """
11 | permission_classes = [permissions.IsAuthenticatedOrReadOnly]
12 | serializer_class = LikeSerializer
13 | queryset = Like.objects.all()
14 |
15 | def perform_create(self, serializer):
16 | serializer.save(owner=self.request.user)
17 |
18 |
19 | class LikeDetail(generics.RetrieveDestroyAPIView):
20 | """
21 | Retrieve a like or delete it by id if you own it.
22 | """
23 | permission_classes = [IsOwnerOrReadOnly]
24 | serializer_class = LikeSerializer
25 | queryset = Like.objects.all()
26 |
--------------------------------------------------------------------------------
/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', 'drf_api.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/posts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/posts/__init__.py
--------------------------------------------------------------------------------
/posts/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/posts/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class PostsConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'posts'
7 |
--------------------------------------------------------------------------------
/posts/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/posts/migrations/__init__.py
--------------------------------------------------------------------------------
/posts/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class Post(models.Model):
6 | """
7 | Post model, related to 'owner', i.e. a User instance.
8 | Default image set so that we can always reference image.url.
9 | """
10 | image_filter_choices = [
11 | ('_1977', '1977'),
12 | ('brannan', 'Brannan'),
13 | ('earlybird', 'Earlybird'),
14 | ('hudson', 'Hudson'),
15 | ('inkwell', 'Inkwell'),
16 | ('lofi', 'Lo-Fi'),
17 | ('kelvin', 'Kelvin'),
18 | ('normal', 'Normal'),
19 | ('nashville', 'Nashville'),
20 | ('rise', 'Rise'),
21 | ('toaster', 'Toaster'),
22 | ('valencia', 'Valencia'),
23 | ('walden', 'Walden'),
24 | ('xpro2', 'X-pro II')
25 | ]
26 | owner = models.ForeignKey(User, on_delete=models.CASCADE)
27 | created_at = models.DateTimeField(auto_now_add=True)
28 | updated_at = models.DateTimeField(auto_now=True)
29 | title = models.CharField(max_length=255)
30 | content = models.TextField(blank=True)
31 | image = models.ImageField(
32 | upload_to='images/', default='../default_post_rgq6aq', blank=True
33 | )
34 | image_filter = models.CharField(
35 | max_length=32, choices=image_filter_choices, default='normal'
36 | )
37 |
38 | class Meta:
39 | ordering = ['-created_at']
40 |
41 | def __str__(self):
42 | return f'{self.id} {self.title}'
43 |
--------------------------------------------------------------------------------
/posts/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from posts.models import Post
3 | from likes.models import Like
4 |
5 |
6 | class PostSerializer(serializers.ModelSerializer):
7 | owner = serializers.ReadOnlyField(source='owner.username')
8 | is_owner = serializers.SerializerMethodField()
9 | profile_id = serializers.ReadOnlyField(source='owner.profile.id')
10 | profile_image = serializers.ReadOnlyField(source='owner.profile.image.url')
11 | like_id = serializers.SerializerMethodField()
12 | likes_count = serializers.ReadOnlyField()
13 | comments_count = serializers.ReadOnlyField()
14 |
15 | def validate_image(self, value):
16 | if value.size > 2 * 1024 * 1024:
17 | raise serializers.ValidationError('Image size larger than 2MB!')
18 | if value.image.height > 4096:
19 | raise serializers.ValidationError(
20 | 'Image height larger than 4096px!'
21 | )
22 | if value.image.width > 4096:
23 | raise serializers.ValidationError(
24 | 'Image width larger than 4096px!'
25 | )
26 | return value
27 |
28 | def get_is_owner(self, obj):
29 | request = self.context['request']
30 | return request.user == obj.owner
31 |
32 | def get_like_id(self, obj):
33 | user = self.context['request'].user
34 | if user.is_authenticated:
35 | like = Like.objects.filter(
36 | owner=user, post=obj
37 | ).first()
38 | return like.id if like else None
39 | return None
40 |
41 | class Meta:
42 | model = Post
43 | fields = [
44 | 'id', 'owner', 'is_owner', 'profile_id',
45 | 'profile_image', 'created_at', 'updated_at',
46 | 'title', 'content', 'image', 'image_filter',
47 | 'like_id', 'likes_count', 'comments_count',
48 | ]
49 |
--------------------------------------------------------------------------------
/posts/tests.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from .models import Post
3 | from rest_framework import status
4 | from rest_framework.test import APITestCase
5 |
6 |
7 | class PostListViewTests(APITestCase):
8 | def setUp(self):
9 | User.objects.create_user(username='adam', password='pass')
10 |
11 | def test_can_list_posts(self):
12 | adam = User.objects.get(username='adam')
13 | Post.objects.create(owner=adam, title='a title')
14 | response = self.client.get('/posts/')
15 | self.assertEqual(response.status_code, status.HTTP_200_OK)
16 | print(response.data)
17 | print(len(response.data))
18 |
19 | def test_logged_in_user_can_create_post(self):
20 | self.client.login(username='adam', password='pass')
21 | response = self.client.post('/posts/', {'title': 'a title'})
22 | count = Post.objects.count()
23 | self.assertEqual(count, 1)
24 | self.assertEqual(response.status_code, status.HTTP_201_CREATED)
25 |
26 | def test_user_not_logged_in_cant_create_post(self):
27 | response = self.client.post('/posts/', {'title': 'a title'})
28 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
29 |
30 |
31 | class PostDetailViewTests(APITestCase):
32 | def setUp(self):
33 | adam = User.objects.create_user(username='adam', password='pass')
34 | brian = User.objects.create_user(username='brian', password='pass')
35 | Post.objects.create(
36 | owner=adam, title='a title', content='adams content'
37 | )
38 | Post.objects.create(
39 | owner=brian, title='another title', content='brians content'
40 | )
41 |
42 | def test_can_retrieve_post_using_valid_id(self):
43 | response = self.client.get('/posts/1/')
44 | self.assertEqual(response.data['title'], 'a title')
45 | self.assertEqual(response.status_code, status.HTTP_200_OK)
46 |
47 | def test_cant_retrieve_post_using_invalid_id(self):
48 | response = self.client.get('/posts/999/')
49 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
50 |
51 | def test_user_can_update_own_post(self):
52 | self.client.login(username='adam', password='pass')
53 | response = self.client.put('/posts/1/', {'title': 'a new title'})
54 | post = Post.objects.filter(pk=1).first()
55 | self.assertEqual(post.title, 'a new title')
56 | self.assertEqual(response.status_code, status.HTTP_200_OK)
57 |
58 | def test_user_cant_update_another_users_post(self):
59 | self.client.login(username='adam', password='pass')
60 | response = self.client.put('/posts/2/', {'title': 'a new title'})
61 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
62 |
--------------------------------------------------------------------------------
/posts/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from posts import views
3 |
4 | urlpatterns = [
5 | path('posts/', views.PostList.as_view()),
6 | path('posts//', views.PostDetail.as_view())
7 | ]
8 |
--------------------------------------------------------------------------------
/posts/views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Count
2 | from rest_framework import generics, permissions, filters
3 | from django_filters.rest_framework import DjangoFilterBackend
4 | from drf_api.permissions import IsOwnerOrReadOnly
5 | from .models import Post
6 | from .serializers import PostSerializer
7 |
8 |
9 | class PostList(generics.ListCreateAPIView):
10 | """
11 | List posts or create a post if logged in
12 | The perform_create method associates the post with the logged in user.
13 | """
14 | serializer_class = PostSerializer
15 | permission_classes = [permissions.IsAuthenticatedOrReadOnly]
16 | queryset = Post.objects.annotate(
17 | likes_count=Count('likes', distinct=True),
18 | comments_count=Count('comment', distinct=True)
19 | ).order_by('-created_at')
20 | filter_backends = [
21 | filters.OrderingFilter,
22 | filters.SearchFilter,
23 | DjangoFilterBackend,
24 | ]
25 | filterset_fields = [
26 | 'owner__followed__owner__profile',
27 | 'likes__owner__profile',
28 | 'owner__profile',
29 | ]
30 | search_fields = [
31 | 'owner__username',
32 | 'title',
33 | ]
34 | ordering_fields = [
35 | 'likes_count',
36 | 'comments_count',
37 | 'likes__created_at',
38 | ]
39 |
40 | def perform_create(self, serializer):
41 | serializer.save(owner=self.request.user)
42 |
43 |
44 | class PostDetail(generics.RetrieveUpdateDestroyAPIView):
45 | """
46 | Retrieve a post and edit or delete it if you own it.
47 | """
48 | serializer_class = PostSerializer
49 | permission_classes = [IsOwnerOrReadOnly]
50 | queryset = Post.objects.annotate(
51 | likes_count=Count('likes', distinct=True),
52 | comments_count=Count('comment', distinct=True)
53 | ).order_by('-created_at')
54 |
--------------------------------------------------------------------------------
/profiles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/profiles/__init__.py
--------------------------------------------------------------------------------
/profiles/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Profile
3 |
4 | admin.site.register(Profile)
5 |
--------------------------------------------------------------------------------
/profiles/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ProfilesConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'profiles'
7 |
--------------------------------------------------------------------------------
/profiles/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Institute-Solutions/drf-api/44acd595f9546ed9127f0dc5ea82b16ed8f0a2c0/profiles/migrations/__init__.py
--------------------------------------------------------------------------------
/profiles/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models.signals import post_save
3 | from django.contrib.auth.models import User
4 |
5 |
6 | class Profile(models.Model):
7 | owner = models.OneToOneField(User, on_delete=models.CASCADE)
8 | created_at = models.DateTimeField(auto_now_add=True)
9 | updated_at = models.DateTimeField(auto_now=True)
10 | name = models.CharField(max_length=255, blank=True)
11 | content = models.TextField(blank=True)
12 | image = models.ImageField(
13 | upload_to='images/', default='../default_profile_qdjgyp'
14 | )
15 |
16 | class Meta:
17 | ordering = ['-created_at']
18 |
19 | def __str__(self):
20 | return f"{self.owner}'s profile"
21 |
22 |
23 | def create_profile(sender, instance, created, **kwargs):
24 | if created:
25 | Profile.objects.create(owner=instance)
26 |
27 |
28 | post_save.connect(create_profile, sender=User)
29 |
--------------------------------------------------------------------------------
/profiles/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import Profile
3 | from followers.models import Follower
4 |
5 |
6 | class ProfileSerializer(serializers.ModelSerializer):
7 | owner = serializers.ReadOnlyField(source='owner.username')
8 | is_owner = serializers.SerializerMethodField()
9 | following_id = serializers.SerializerMethodField()
10 | posts_count = serializers.ReadOnlyField()
11 | followers_count = serializers.ReadOnlyField()
12 | following_count = serializers.ReadOnlyField()
13 |
14 | def get_is_owner(self, obj):
15 | request = self.context['request']
16 | return request.user == obj.owner
17 |
18 | def get_following_id(self, obj):
19 | user = self.context['request'].user
20 | if user.is_authenticated:
21 | following = Follower.objects.filter(
22 | owner=user, followed=obj.owner
23 | ).first()
24 | # print(following)
25 | return following.id if following else None
26 | return None
27 |
28 | class Meta:
29 | model = Profile
30 | fields = [
31 | 'id', 'owner', 'created_at', 'updated_at', 'name',
32 | 'content', 'image', 'is_owner', 'following_id',
33 | 'posts_count', 'followers_count', 'following_count',
34 | ]
35 |
--------------------------------------------------------------------------------
/profiles/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/profiles/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from profiles import views
3 |
4 | urlpatterns = [
5 | path('profiles/', views.ProfileList.as_view()),
6 | path('profiles//', views.ProfileDetail.as_view()),
7 | ]
8 |
--------------------------------------------------------------------------------
/profiles/views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Count
2 | from rest_framework import generics, filters
3 | from django_filters.rest_framework import DjangoFilterBackend
4 | from drf_api.permissions import IsOwnerOrReadOnly
5 | from .models import Profile
6 | from .serializers import ProfileSerializer
7 |
8 |
9 | class ProfileList(generics.ListAPIView):
10 | """
11 | List all profiles.
12 | No create view as profile creation is handled by django signals.
13 | """
14 | queryset = Profile.objects.annotate(
15 | posts_count=Count('owner__post', distinct=True),
16 | followers_count=Count('owner__followed', distinct=True),
17 | following_count=Count('owner__following', distinct=True)
18 | ).order_by('-created_at')
19 | serializer_class = ProfileSerializer
20 | filter_backends = [
21 | filters.OrderingFilter,
22 | DjangoFilterBackend,
23 | ]
24 | filterset_fields = [
25 | 'owner__following__followed__profile',
26 | 'owner__followed__owner__profile',
27 | ]
28 | ordering_fields = [
29 | 'posts_count',
30 | 'followers_count',
31 | 'following_count',
32 | 'owner__following__created_at',
33 | 'owner__followed__created_at',
34 | ]
35 |
36 |
37 | class ProfileDetail(generics.RetrieveUpdateAPIView):
38 | """
39 | Retrieve or update a profile if you're the owner.
40 | """
41 | permission_classes = [IsOwnerOrReadOnly]
42 | queryset = Profile.objects.annotate(
43 | posts_count=Count('owner__post', distinct=True),
44 | followers_count=Count('owner__followed', distinct=True),
45 | following_count=Count('owner__following', distinct=True)
46 | ).order_by('-created_at')
47 | serializer_class = ProfileSerializer
48 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | asgiref==3.3.4
2 | cloudinary==1.25.0
3 | dj-database-url==0.5.0
4 | dj-rest-auth==2.1.9
5 | Django==3.2.4
6 | django-allauth==0.44.0
7 | django-cloudinary-storage==0.3.0
8 | django-cors-headers==3.7.0
9 | django-filter==2.4.0
10 | djangorestframework==3.12.4
11 | djangorestframework-simplejwt==4.7.2
12 | gunicorn==20.1.0
13 | oauthlib==3.1.1
14 | Pillow==8.2.0
15 | psycopg2==2.9.1
16 | PyJWT==2.1.0
17 | python3-openid==3.2.0
18 | pytz==2021.1
19 | requests-oauthlib==1.3.0
20 | sqlparse==0.4.1
21 |
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-3.9.18
--------------------------------------------------------------------------------