From 7d5b7732fe5db867b28f827b6b318ea594631d0b Mon Sep 17 00:00:00 2001 From: Vinnie Magro Date: Tue, 19 Sep 2017 22:37:27 -0700 Subject: [PATCH 1/5] Fresh start --- server/social_network/admin.py | 7 --- server/social_network/models.py | 48 --------------------- server/social_network/serializers.py | 40 ----------------- server/social_network/urls.py | 14 ------ server/social_network/views.py | 64 ---------------------------- 5 files changed, 173 deletions(-) diff --git a/server/social_network/admin.py b/server/social_network/admin.py index def86f5..3890675 100644 --- a/server/social_network/admin.py +++ b/server/social_network/admin.py @@ -1,10 +1,3 @@ """Admin-site customization for social_network.""" from django.contrib import admin - -from .models import Profile, Post, Vote -# We can register models here so that they can be edited in the Django admin site - -admin.site.register(Profile) -admin.site.register(Post) -admin.site.register(Vote) diff --git a/server/social_network/models.py b/server/social_network/models.py index 8b36771..7d542c1 100644 --- a/server/social_network/models.py +++ b/server/social_network/models.py @@ -1,51 +1,3 @@ """Models for the social_network.""" from django.db import models -from django.contrib.auth.models import User - - -class Profile(models.Model): - """Profile is a Model that stores application-specific data about a user. - - This is not used directly for authentication/authorization, instead the - Django default User model is used for these purposes. - """ - - user = models.OneToOneField(User, on_delete=models.CASCADE) - created = models.DateTimeField(auto_now_add=True) - username = models.CharField(max_length=200) - picture_url = models.URLField() - - def __str__(self): - """Return a human-readable description of this Profile.""" - return 'Profile: {}'.format(self.username) - - -class Post(models.Model): - """Post is an individual post on the social media site. - - A Post is simply a text message associated with a user Profile. - """ - - # A Post has an owner Profile reference, this is a Many-to-One relationship, all Posts have - # exactly one Profile, and a Profile has 0 or more Posts, the `related_name` argument means that - # all of a Profile instance's Posts can be accessed as `profile.posts` - owner = models.ForeignKey(Profile, related_name='posts', on_delete=models.CASCADE) - - created = models.DateTimeField(auto_now_add=True) - title = models.CharField(max_length=200) - - def __str__(self): - """Return a human-readable description of this Post.""" - return 'Post: "{}" by {}'.format(self.title, self.owner.username) - - -class Vote(models.Model): - """Vote maps a Profile upvoting a Post.""" - - profile = models.ForeignKey(Profile, on_delete=models.CASCADE) - post = models.ForeignKey(Post, on_delete=models.CASCADE) - - def __str__(self): - """Return a human-readable description of this Vote.""" - return 'Vote: {} for "{}"'.format(self.profile.username, self.post.title) diff --git a/server/social_network/serializers.py b/server/social_network/serializers.py index 0bc7757..983f66b 100644 --- a/server/social_network/serializers.py +++ b/server/social_network/serializers.py @@ -5,43 +5,3 @@ """ from rest_framework import serializers - -from .models import Post, Profile - - -class PostSerializer(serializers.HyperlinkedModelSerializer): - """Serializes Posts. - - This is an example of a semi-customized Serializer class, for simple Models, the ModelSerializer - is capable of doing just about everything we want, but in this case we want to have a "computed" - field that counts how many votes a post has received. - """ - - class Meta: - """Model/fields for ModelSerializer.""" - - model = Post - fields = ['id', 'url', 'created', 'owner', 'owner_name', 'title', 'vote_count'] - - vote_count = serializers.SerializerMethodField() - owner = serializers.HyperlinkedRelatedField(view_name='profile-detail', read_only=True) - owner_name = serializers.ReadOnlyField(source='owner.username') - - def get_vote_count(self, post): - """Count how many votes the `Post` has received.""" - return post.vote_set.count() - - -class ProfileSerializer(serializers.HyperlinkedModelSerializer): - """Serializes user Profiles.""" - - class Meta: - """Model/fields for ModelSerializer.""" - - model = Profile - fields = ['id', 'url', 'username', 'posts'] - - # this is a Django backwards relation mapping Profile -> Posts, so it won't be included by - # default by the ModelSerializer, so we have to add it manually - # Use the PostSerializer so that the Post objects will be fully instantiated - posts = PostSerializer(many=True, read_only=True) diff --git a/server/social_network/urls.py b/server/social_network/urls.py index 585eb6c..0b4a123 100644 --- a/server/social_network/urls.py +++ b/server/social_network/urls.py @@ -1,20 +1,6 @@ """URLs specific to the social_network app.""" from django.conf.urls import include, url -from rest_framework.routers import DefaultRouter -from rest_framework.schemas import get_schema_view - -from . import views - -router = DefaultRouter(trailing_slash=False) -router.register(r'profiles', views.ProfileViewSet) -router.register(r'posts', views.PostViewSet) - -# This generates a coreapi schema that can be used by coreapi clients -schema_view = get_schema_view(title='social_network API') urlpatterns = [ - url(r'^', include(router.urls)), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - url(r'^schema/$', schema_view), ] diff --git a/server/social_network/views.py b/server/social_network/views.py index c07c10a..a75bf6e 100644 --- a/server/social_network/views.py +++ b/server/social_network/views.py @@ -1,65 +1 @@ """API views for social_network.""" - -from rest_framework import viewsets -from rest_framework.decorators import api_view, detail_route -from rest_framework.response import Response -from rest_framework.reverse import reverse - -from .models import Profile, Post, Vote -from .serializers import ProfileSerializer, PostSerializer - - -@api_view(['GET']) -def api_root(request, format=None): - """Root of API, this is useful for documentation generated by DRF.""" - return Response({ - 'profiles': reverse('profile-list', request=request, format=format), - 'posts': reverse('post-list', request=request, format=format) - }) - - -class ProfileViewSet(viewsets.ReadOnlyModelViewSet): - """This provides get and list functionality for Profiles.""" - - queryset = Profile.objects.all() - serializer_class = ProfileSerializer - - -class PostViewSet(viewsets.ModelViewSet): - """Get or create Posts. - - retrieve: - Return a post given its ID. - - list: - Get a paginated list of all Posts. - - create: - Create a new Post as the logged-in user. - """ - - queryset = Post.objects.all().order_by('-created') - serializer_class = PostSerializer - - def perform_create(self, serializer): - """Create a Post associated with the logged-in user.""" - serializer.save(owner=self.request.user.profile) - - @detail_route(methods=['POST', 'DELETE'], url_path='vote') - def vote(self, request, pk=None): - """Vote or unvote on a post.""" - post = self.get_object() - if request.method == 'POST': - # check if the vote already exists, if so don't allow the user to vote again - if Vote.objects.filter(profile=self.request.user.profile, post=post).exists(): - # the user already voted, just return the post directly - data = PostSerializer(post, context={'request': self.request}).data - return Response(data) - new_vote = Vote(profile=self.request.user.profile, post=post) - new_vote.save() - - elif request.method == 'DELETE': - Vote.objects.filter(profile=self.request.user.profile, post=post).delete() - - data = PostSerializer(post, context={'request': self.request}).data - return Response(data) From 3a8eed8d949ccf0a0ea99fa8898d111ee020d3e3 Mon Sep 17 00:00:00 2001 From: Vinnie Magro Date: Tue, 19 Sep 2017 22:38:55 -0700 Subject: [PATCH 2/5] Create models for Profiles, Posts and Votes To build our social network application, we need to store data. We store data in a database using Django Models. These models are defined in models.py Here we create 3 models: Profile - stores data about a User such as their username and picture Post - an individual post on our social network. Posts are simply a string of text associated with a Profile Vote - an upvote on a Post: any Profile can add a Vote associated with any Post --- server/social_network/models.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/server/social_network/models.py b/server/social_network/models.py index 7d542c1..f5e669e 100644 --- a/server/social_network/models.py +++ b/server/social_network/models.py @@ -1,3 +1,61 @@ """Models for the social_network.""" from django.db import models +from django.contrib.auth.models import User + + +class Profile(models.Model): + """Profile is a Model that stores application-specific data about a user. + + This is not used directly for authentication/authorization, instead the + Django default User model is used for these purposes. + """ + + # Django provides a User class built-in, this makes it easy to do things like authentication and + # authorization, so rather than creat our own User model to replace Django's default one, we add + # a one-to-one relationship with a Django User so that we can associate our own data with it + user = models.OneToOneField(User, on_delete=models.CASCADE) + + created = models.DateTimeField(auto_now_add=True) + + username = models.CharField(max_length=200) + picture_url = models.URLField() + + def __str__(self): + """Return a human-readable description of this Profile.""" + return 'Profile: {}'.format(self.username) + + +class Post(models.Model): + """Post is an individual post on the social media site. + + A Post is simply a text message associated with a user Profile. + """ + + # A Post has an owner Profile reference, this is a Many-to-One relationship, all Posts have + # exactly one Profile, and a Profile has 0 or more Posts, the `related_name` argument means that + # all of a Profile instance's Posts can be accessed as `profile.posts` + owner = models.ForeignKey(Profile, related_name='posts', on_delete=models.CASCADE) + + # we store when the Post was created so that we can sort them by date in the UI + created = models.DateTimeField(auto_now_add=True) + + # the actual text content of the Post + title = models.CharField(max_length=200) + + def __str__(self): + """Return a human-readable description of this Post.""" + return 'Post: "{}" by {}'.format(self.title, self.owner.username) + + +class Vote(models.Model): + """Vote maps a Profile upvoting a Post.""" + + # Reference to the Profile that "owns" this Vote + profile = models.ForeignKey(Profile, on_delete=models.CASCADE) + # Reference to the Post that this Vote belongs to + post = models.ForeignKey(Post, on_delete=models.CASCADE) + + def __str__(self): + """Return a human-readable description of this Vote.""" + return 'Vote: {} for "{}"'.format(self.profile.username, self.post.title) From b430783d2dae314d1fd4fa3243b0119339479ba2 Mon Sep 17 00:00:00 2001 From: Vinnie Magro Date: Tue, 19 Sep 2017 22:47:58 -0700 Subject: [PATCH 3/5] Add models to admin site We want to add our new models to the Django admin site so that we can start to create, edit, and delete models before we finish implementing the rest of the our API. Once you add these lines, go to the admin interface and see that they were added! http://localhost:8000/admin --- server/social_network/admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/social_network/admin.py b/server/social_network/admin.py index 3890675..def86f5 100644 --- a/server/social_network/admin.py +++ b/server/social_network/admin.py @@ -1,3 +1,10 @@ """Admin-site customization for social_network.""" from django.contrib import admin + +from .models import Profile, Post, Vote +# We can register models here so that they can be edited in the Django admin site + +admin.site.register(Profile) +admin.site.register(Post) +admin.site.register(Vote) From 6f2159b0fa8bea5ace166dfeede13ec7ee7035df Mon Sep 17 00:00:00 2001 From: Vinnie Magro Date: Tue, 19 Sep 2017 22:57:13 -0700 Subject: [PATCH 4/5] Create PostSerializer Create a Django REST Framework Serializer class for Post objects. This allows us to create/read/update/list/delete Posts in our database. At this point we don't have a simple way to actually run this code, but in the next task we'll add API endpoints to actually use our PostSerializer class --- server/social_network/serializers.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/social_network/serializers.py b/server/social_network/serializers.py index 983f66b..e9d9054 100644 --- a/server/social_network/serializers.py +++ b/server/social_network/serializers.py @@ -5,3 +5,23 @@ """ from rest_framework import serializers + +from .models import Post + + +class PostSerializer(serializers.Serializer): + """Serialize a Post to/from the database.""" + + title = serializers.CharField(max_length=200) + created = serializers.DateTimeField() + + def create(self, validated_data): + """Create and return a new `Post` instance, given the validated data.""" + return Post.objects.create(**validated_data) + + def update(self, instance, validated_data): + """Update and return an existing `Post` instance, given the validated data.""" + instance.title = validated_data.get('title', instance.title) + instance.created = validated_data.get('created', instance.style) + instance.save() + return instance From aabffa9d6d0fbe351d49b2bad138f15545012b2f Mon Sep 17 00:00:00 2001 From: Vinnie Magro Date: Tue, 19 Sep 2017 23:01:44 -0700 Subject: [PATCH 5/5] Create views for posts In the last task, we added a serializer to convert Posts to and from our API representations. After adding the code in this task, use your favorite REST interface (such as Postman) to try some requests on http://localhost:8000/api/posts - at this point we've implemented support for listing all posts (GET to /api/posts), creating a post (POST to /api/posts), getting an individual post by ID (GET to /api/posts/), updating a post (PUT to /api/posts/) and deleting a post (DELETE to /api/posts/) As you might have noticed, that's a lot of code to do not a whole lot. Fortunately Django REST Framework provides a lot of conveniences for us that we'll explore in later tasks. --- server/social_network/urls.py | 4 +++ server/social_network/views.py | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/server/social_network/urls.py b/server/social_network/urls.py index 0b4a123..5928aee 100644 --- a/server/social_network/urls.py +++ b/server/social_network/urls.py @@ -2,5 +2,9 @@ from django.conf.urls import include, url +from . import views + urlpatterns = [ + url(r'^posts/?$', views.post_list), + url(r'^posts/(?P[0-9]+)/$', views.post_detail), ] diff --git a/server/social_network/views.py b/server/social_network/views.py index a75bf6e..c1f6631 100644 --- a/server/social_network/views.py +++ b/server/social_network/views.py @@ -1 +1,51 @@ """API views for social_network.""" + +from django.http import HttpResponse, JsonResponse +from django.views.decorators.csrf import csrf_exempt + +from .models import Post +from .serializers import PostSerializer + + +# we're simplifying things by using csrf_exempt, in a real application we would want the security +# that it provides +@csrf_exempt +def post_list(request): + """List all posts, or create a new post.""" + if request.method == 'GET': + posts = Post.objects.all() + serializer = PostSerializer(posts, many=True) + return JsonResponse(serializer.data, safe=False) + + elif request.method == 'POST': + data = JSONParser().parse(request) + serializer = PostSerializer(data=data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, status=201) + return JsonResponse(serializer.errors, status=400) + + +@csrf_exempt +def post_detail(request, pk): + """Retrieve, update or delete an individual Post.""" + try: + post = Post.objects.get(pk=pk) + except Post.DoesNotExist: + return HttpResponse(status=404) + + if request.method == 'GET': + serializer = PostSerializer(post) + return JsonResponse(serializer.data) + + elif request.method == 'PUT': + data = JSONParser().parse(request) + serializer = PostSerializer(post, data=data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data) + return JsonResponse(serializer.errors, status=400) + + elif request.method == 'DELETE': + post.delete() + return HttpResponse(status=204)