Retweeting Logic:

Or “Comment logic”

  • add “parent” variable in /tweets/models,py
  • at first, the parent of a tweet is itself, and it’s optional

    1. class Tweet(models.Model):
    2. parent = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) # refer to self, when deleted->set into NULL
    3. # ...
  • make migrations and migrate

    1. [root@localhost Twittme]# python3 ./manage.py makemigrations
    2. [root@localhost Twittme]# python3 ./manage.py migrate
  • redirect “retweet” button in /tweets/views.py

    1. @api_view(['POST'])
    2. @permission_classes([IsAuthenticated])
    3. def tweet_action_view(request, *args, **kwargs):
    4. # ...
    5. elif action == "retweet":
    6. new_tweet = Tweet.objects.create(
    7. user=request.user,
    8. parent=obj,
    9. content=content
    10. )
    11. serializer = TweetSerializer(new_tweet)
    12. return Response(serializer.data, status=200)
    13. return Response({}, status=200)
  • add ‘content’ into /tweets/serializers.py

    1. class TweetActionSerializer(serializers.Serializer):
    2. id = serializers.IntegerField()
    3. action = serializers.CharField()
    4. content = serializers.CharField(allow_blank=True, required=False) # new
    5. # ...

    and into /tweets/views.py

    1. @api_view(['POST'])
    2. @permission_classes([IsAuthenticated])
    3. def tweet_action_view(request, *args, **kwargs):
    4. # id is required
    5. # action: like, unlike, retweet
    6. serializer = TweetActionSerializer(data=request.data)
    7. if serializer.is_valid(raise_exception=True):
    8. data = serializer.validated_data
    9. tweet_id = data.get("id")
    10. action = data.get("action")
    11. content = data.get("content") # new
    12. # ...
    13. return Response({}, status=200)

    to test, runserver and click “retweet”
    image.png
    so it only create a new tweet without any content for now.

possible question about migration and solution:

  • when makemigrations and migrate, sometimes the server cannot get the tweet list
  • and shows “no such column: tweets_tweet.parent_id”

possible solution:

  1. declare parent as a constant

    1. class Tweet(models.Model):
    2. # parent = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) # refer to self, when deleted->set into NULL
    3. parent = 0
    4. # ...
  2. make migrations and migrate

    1. [root@localhost Twittme]# python3 ./manage.py makemigrations
    2. [root@localhost Twittme]# python3 ./manage.py migrate
  3. again declare parent as a foreign key instead

    1. class Tweet(models.Model):
    2. parent = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) # refer to self, when deleted->set into NULL
    3. # parent = 0
    4. # ...
  4. make migrations and migrate to substitute

    1. [root@localhost Twittme]# python3 ./manage.py makemigrations
    2. [root@localhost Twittme]# python3 ./manage.py migrate

Two Types of Serializers:

In this step, we will separate serializer into 2 categories

  • update our todo.md to clarify our purpose first ```
  1. Tweets -> User Permissions
    1. -> Creating
    2. -> Text
    3. -> Image -> Media Storage Server
    4. -> Delete
    5. -> Retweeting
    6. -> Read only serializer
    7. -> Create only serializer
    8. -> Liking
    ``` so we need a read only serializer and a create only serializer.
  • in /tweets/serializers.py

    • modify TweetSerializer
    • add TweetCreateSerializer to separate create tweet step ```python class TweetCreateSerializer(serializers.ModelSerializer): # new likes = serializers.SerializerMethodField(read_only=True)

      class Meta: model = Tweet fields = [‘id’, ‘content’, ‘likes’] def get_likes(self, obj): return obj.likes.count()

      def validate_content(self, value): if len(value) > MAX_TWEET_LENGTH:

      1. raise serializers.ValidationError("This tweet is too long")

      return value

class TweetSerializer(serializers.ModelSerializer): likes = serializers.SerializerMethodField(read_only=True) content = serializers.SerializerMethodField(read_only=True) # new class Meta: model = Tweet fields = [‘id’, ‘content’, ‘likes’]

  1. def get_likes(self, obj):
  2. return obj.likes.count()
  3. def get_content(self, obj): # new
  4. return obj.content
  5. #def validate_content(self, value):
  6. # if len(value) > MAX_TWEET_LENGTH:
  7. # raise serializers.ValidationError("This tweet is too long")
  8. # return value
  1. - in /tweets/views.py
  2. - import TweetCreateSerializer
  3. - TweetSerializer -> TweetCreateSerializer for tweet_create_view()
  4. ```python
  5. # ...
  6. from .serializers import (
  7. TweetSerializer,
  8. TweetActionSerializer,
  9. TweetCreateSerializer # new
  10. )
  11. # ...
  12. @api_view(['POST']) # http method the client === POST
  13. @authentication_classes([SessionAuthentication])
  14. @permission_classes([IsAuthenticated])
  15. def tweet_create_view(request, *args, **kwargs):
  16. # serializer = TweetSerializer(data=request.POST)
  17. serializer = TweetCreateSerializer(data=request.POST)
  18. # ...
  19. return Response({}, status=400)

to test, runserver and send something
image.png
so the create tweet function still works after the separation.

  • add a boolean to check it the tweet a retweet in /tweets/models.py

    1. class Tweet(models.Model):
    2. # ...
    3. @property
    4. def is_retweet(self): # new
    5. return self.parent != None
  • add a new field and let the content of retweet become contents of their parents

    1. class TweetSerializer(serializers.ModelSerializer):
    2. class Meta:
    3. model = Tweet
    4. # fields = ['id', 'content', 'likes']
    5. fields = ['id', 'content', 'likes', 'is_retweet']
    6. # ...
    7. def get_content(self, obj):
    8. # return obj.content
    9. content = obj.content
    10. if obj.is_retweet: # is this tweet a retweet
    11. content = obj.parent.content
    12. return content

    to test, runserver and click retweet
    image.png
    image.png
    this time the new tweet has content of their parent content instead of just “null”.

  • However, here we should actually pass the parent relationship into the serializer:

  • modify TweetSerializer in /tweets/serializers.py

    1. class TweetSerializer(serializers.ModelSerializer):
    2. likes = serializers.SerializerMethodField(read_only=True)
    3. #content = serializers.SerializerMethodField(read_only=True)
    4. parent = TweetCreateSerializer(read_only=True)
    5. class Meta:
    6. model = Tweet
    7. # fields = ['id', 'content', 'likes', 'is_retweet']
    8. fields = ['id', 'content', 'likes', 'is_retweet', 'parent']
    9. def get_likes(self, obj):
    10. return obj.likes.count()
    11. #def get_content(self, obj):
    12. # content = obj.content
    13. # if obj.is_retweet:
    14. # content = obj.parent.content
    15. # return content

    to test, runserver and see /tweets
    image.png
    image.png
    so we have serialized “is_retweet” and “parent” for each tweet.

Internal App Urls:

In this part, we gonna try to make urls into a rest api package.

  • create urls.py in /tweets
  • it’s similar to /Twittme/urls.py with some changes ```python from django.contrib import admin

from django.urls import path, re_path

from django.urls import path

from tweets.views import (

from .views import ( home_view, tweet_detail_view, tweet_list_view, tweet_create_view, tweet_delete_view, tweet_action_view, )

urlpatterns = [

  1. # path('admin/', admin.site.urls),
  2. # path('', home_view),
  3. # path('tweets', tweet_list_view),
  4. path('', tweet_list_view),
  5. # path('api/tweets/action', tweet_action_view),
  6. path('action/', tweet_action_view),
  7. # path('create-tweet', tweet_create_view),
  8. path('create/', tweet_create_view),
  9. # path('tweets/<int:tweet_id>', tweet_detail_view),
  10. path('<int:tweet_id>/', tweet_detail_view),
  11. # path('api/tweets/<int:tweet_id>/delete', tweet_delete_view),
  12. path('<int:tweet_id>/delete/', tweet_delete_view),

]

  1. - and add this package into /Twittme/urls.py
  2. - then simplify path logic
  3. ```python
  4. # ...
  5. # from django.urls import path, re_path
  6. from django.urls import path, re_path, include
  7. # ...
  8. urlpatterns = [
  9. path('admin/', admin.site.urls),
  10. path('', home_view),
  11. path('tweets', tweet_list_view),
  12. path('create-tweet', tweet_create_view),
  13. path('tweets/<int:tweet_id>', tweet_detail_view),
  14. # path('api/tweets/<int:tweet_id>/delete', tweet_delete_view),
  15. # path('api/tweets/action', tweet_action_view),
  16. path('api/tweets/', include('tweets.urls')), # new
  17. ]

and those paths about api/tweets/ are still available
image.png
image.png
image.png
image.png
image.png

Setting up Tests in Django:

In this part, setup some tests in django and run them automatically.
run tests by terminal command:

  1. python3 ./manage.py test
  • to test app “tweets”, modify /tweets/tests.py ```python from django.contrib.auth import get_user_model from django.test import TestCase

from rest_framework.test import APIClient

from .models import Tweet

Create your tests here.

User = get_user_model()

class TweetTestCase(TestCase): def setUp(self): # setup 2 users and 3 tweets self.user = User.objects.create_user(username=’cfe’, password=’somepassword’) self.userb = User.objects.create_user(username=’cfe-2’, password=’somepassword2’) Tweet.objects.create(content=”my first tweet”, user=self.user) Tweet.objects.create(content=”my first tweet”, user=self.user) Tweet.objects.create(content=”my first tweet”, user=self.userb) self.currentCount = Tweet.objects.all().count()

  1. def test_tweet_created(self): # is the tweet correctly created
  2. tweet_obj = Tweet.objects.create(content="my second tweet",
  3. user=self.user)
  4. self.assertEqual(tweet_obj.id, 4)
  5. self.assertEqual(tweet_obj.user, self.user)
  6. def get_client(self): # could it get the right client
  7. client = APIClient()
  8. client.login(username=self.user.username, password='somepassword')
  9. return client
  10. def test_tweet_list(self): # could it get the right tweet list (for "user")
  11. client = self.get_client()
  12. response = client.get("/api/tweets/")
  13. self.assertEqual(response.status_code, 200)
  14. self.assertEqual(len(response.json()), 3)
  15. def test_action_like(self): # is the like action done successfully
  16. client = self.get_client()
  17. response = client.post("/api/tweets/action/",
  18. {"id": 1, "action": "like"})
  19. self.assertEqual(response.status_code, 200)
  20. like_count = response.json().get("likes")
  21. self.assertEqual(like_count, 1)
  22. def test_action_unlike(self): # is the unlike action done successfully
  23. client = self.get_client()
  24. response = client.post("/api/tweets/action/",
  25. {"id": 2, "action": "like"})
  26. self.assertEqual(response.status_code, 200)
  27. response = client.post("/api/tweets/action/",
  28. {"id": 2, "action": "unlike"})
  29. self.assertEqual(response.status_code, 200)
  30. like_count = response.json().get("likes")
  31. self.assertEqual(like_count, 0)
  32. def test_action_retweet(self): # is the retweet action done successfully
  33. client = self.get_client()
  34. response = client.post("/api/tweets/action/",
  35. {"id": 2, "action": "retweet"})
  36. self.assertEqual(response.status_code, 201)
  37. data = response.json()
  38. new_tweet_id = data.get("id")
  39. self.assertNotEqual(2, new_tweet_id)
  40. self.assertEqual(self.currentCount + 1, new_tweet_id)
  41. def test_tweet_create_api_view(self): # could get the create api view?
  42. request_data = {"content": "This is my test tweet"}
  43. client = self.get_client()
  44. response = client.post("/api/tweets/create/", request_data)
  45. self.assertEqual(response.status_code, 201)
  46. response_data = response.json()
  47. new_tweet_id = response_data.get("id")
  48. self.assertEqual(self.currentCount + 1, new_tweet_id)
  49. def test_tweet_detail_api_view(self): # could get the detail api view?
  50. client = self.get_client()
  51. response = client.get("/api/tweets/1/")
  52. self.assertEqual(response.status_code, 200)
  53. data = response.json()
  54. _id = data.get("id")
  55. self.assertEqual(_id, 1)
  56. def test_tweet_delete_api_view(self): # could get the delete api view?
  57. client = self.get_client()
  58. response = client.delete("/api/tweets/1/delete/")
  59. self.assertEqual(response.status_code, 200)
  60. client = self.get_client()
  61. response = client.delete("/api/tweets/1/delete/")
  62. self.assertEqual(response.status_code, 404)
  63. response_incorrect_owner = client.delete("/api/tweets/3/delete/")
  64. self.assertEqual(response_incorrect_owner.status_code, 401)
  1. - to make sure the test can get enough response, add some details in /tweets/views.py
  2. ```python
  3. @api_view(['POST'])
  4. @permission_classes([IsAuthenticated])
  5. def tweet_action_view(request, *args, **kwargs):
  6. serializer = TweetActionSerializer(data=request.data)
  7. if serializer.is_valid(raise_exception=True):
  8. # ...
  9. if action == "like":
  10. # ...
  11. elif action == "unlike":
  12. obj.likes.remove(request.user)
  13. serializer = TweetSerializer(obj) # new
  14. return Response(serializer.data, status=200) # new
  15. elif action == "retweet":
  16. # ...
  17. # return Response(serializer.data, status=200)
  18. return Response(serializer.data, status=201) # status code from 200 to 201
  19. return Response({}, status=200)

to test, use

  1. [root@localhost Twittme]# python3 ./manage.py test
  2. Creating test database for alias 'default'...
  3. System check identified no issues (0 silenced).
  4. ........
  5. ----------------------------------------------------------------------
  6. Ran 8 tests in 8.196s
  7. OK
  8. Destroying test database for alias 'default'...

so the process is a test database is created and used to test all 8 test cases (functions with test_ prefix) above.

Here we create a fail case, change /tweets/urls.py only for this test

  1. urlpatterns = [
  2. # ...
  3. # path('action/', tweet_action_view),
  4. path('actions/', tweet_action_view),
  5. # ...
  6. ]

to test

  1. [root@localhost Twittme]# python3 ./manage.py test
  2. Creating test database for alias 'default'...
  3. System check identified no issues (0 silenced).
  4. FFF.....
  5. ======================================================================
  6. FAIL: test_action_like (tweets.tests.TweetTestCase)
  7. ----------------------------------------------------------------------
  8. Traceback (most recent call last):
  9. File "/root/SourceCode/Python/reactjs/Twittme/tweets/tests.py", line 49, in test_action_like
  10. self.assertEqual(response.status_code, 200)
  11. AssertionError: 404 != 200
  12. ======================================================================
  13. FAIL: test_action_retweet (tweets.tests.TweetTestCase)
  14. ----------------------------------------------------------------------
  15. Traceback (most recent call last):
  16. File "/root/SourceCode/Python/reactjs/Twittme/tweets/tests.py", line 68, in test_action_retweet
  17. self.assertEqual(response.status_code, 201)
  18. AssertionError: 404 != 201
  19. ======================================================================
  20. FAIL: test_action_unlike (tweets.tests.TweetTestCase)
  21. ----------------------------------------------------------------------
  22. Traceback (most recent call last):
  23. File "/root/SourceCode/Python/reactjs/Twittme/tweets/tests.py", line 57, in test_action_unlike
  24. self.assertEqual(response.status_code, 200)
  25. AssertionError: 404 != 200
  26. ----------------------------------------------------------------------
  27. Ran 8 tests in 8.164s
  28. FAILED (failures=3)
  29. Destroying test database for alias 'default'...

so because url has been changes, 3 tests about “action” got failed, and detail lines are shown.

Under “System check identified no issues (0 silenced).”, we can see 8 letters corresponding to 8 test cases.

  • all clear:

“……..”

  • 3 fails:

“FFF…..”

So “.” means that test passed, “F” means failed.

  • Remember to recover urls in /tweets/urls.py, and do the test again to check.

Verify or Install Node.js:

methods to install node.js varies.

  • nodejs homepage: https://nodejs.org/en/

  • I use “nvm(Node Version Manager)” to install node.js ```bash yum update

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.1/install.sh | bash

nvm —version

0.35.1

nvm ls-remote # check available versions

nvm install 12.18.1

  1. check versions:
  2. ```bash
  3. [root@localhost ~]# npm --version
  4. 6.14.5
  5. [root@localhost ~]# nvm --version
  6. 0.35.1
  7. [root@localhost ~]# node --version
  8. v12.18.1