Skip to content
This repository has been archived by the owner on Mar 30, 2019. It is now read-only.

Backend / Event and related endpoints #95

Merged
merged 21 commits into from
Oct 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
490a16c
Create event and related models
mertakozcan Oct 10, 2018
7056e15
Install `django_extensions` for `shell_plus`
mertakozcan Oct 10, 2018
963adfa
Install `isort` for sorting import statements
mertakozcan Oct 11, 2018
ccafeeb
Revision for model fields
mertakozcan Oct 11, 2018
1c52de9
Implement basic API endpoints for `Event`, `Tag`, `Comment` and `Media`
mertakozcan Oct 11, 2018
5bcdde7
Remove migration files
mertakozcan Oct 16, 2018
82089e1
Implement `AttendanceStatus` and generic models && Add `owner` fields
mertakozcan Oct 21, 2018
a56a65f
Implement a custom permission to check ownership before certain actions
mertakozcan Oct 21, 2018
4ab91be
Update `serializers` and `views` to support authenticated requests
mertakozcan Oct 21, 2018
6fae3c1
Make `Comment` model generic to support user and event comments
mertakozcan Oct 21, 2018
eafc8d4
Typo
mertakozcan Oct 23, 2018
8bd2170
Implement `attendance` endpoint && Optimize view-based classes
mertakozcan Oct 23, 2018
19cd9c5
Implement `follow` functionality and its API endpoint
mertakozcan Oct 24, 2018
1612176
Create signals to update `follower_count`
mertakozcan Oct 24, 2018
d72cbe7
Implement mixins for models && Add `DELETE` for `AttendanceStatus`
mertakozcan Oct 25, 2018
87adb79
General fixes && Start implementation of `vote` endpoint
mertakozcan Oct 25, 2018
60baff9
Implement `vote` endpoint
mertakozcan Oct 26, 2018
9c7001b
Rename
mertakozcan Oct 26, 2018
b307592
Fix event `create` and `update` issues && Revision for serializers
mertakozcan Oct 27, 2018
cd7c4c4
Inherit View classes from `views` instead of `mixins`
mertakozcan Oct 27, 2018
766254a
Update urls to support `DELETE` for `attendance`, `follow` and `vote`
mertakozcan Oct 28, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'api.apps.ApiConfig'
3 changes: 3 additions & 0 deletions backend/api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

class ApiConfig(AppConfig):
name = 'api'

def ready(self):
import api.handlers
17 changes: 17 additions & 0 deletions backend/api/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import F
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver

from api.models import FollowStatus


@receiver(post_save, sender=FollowStatus, dispatch_uid='increment_follower_count')
def increment_follower_count(sender, instance, **kwargs):
instance.content_type.model_class().objects.\
filter(id=instance.object_id).update(follower_count=F('follower_count')+1)

@receiver(pre_delete, sender=FollowStatus, dispatch_uid='decrement_follower_count')
def decrement_follower_count(sender, instance, **kwargs):
instance.content_type.model_class().objects.\
filter(id=instance.object_id).update(follower_count=F('follower_count')-1)
173 changes: 172 additions & 1 deletion backend/api/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,174 @@
from django.conf import settings
from django.contrib.contenttypes.fields import (GenericForeignKey,
GenericRelation)
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone

# Create your models here.

class OwnerMixin(models.Model):
"""
Each model that belongs to a `User` must use this mixin.
"""
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='%(class)s_set', on_delete=models.CASCADE)

class Meta:
abstract = True


class CommentMixin(models.Model):
"""
Each model that contains `Comment`s must use this mixin.
"""
comments = GenericRelation('Comment')

class Meta:
abstract = True


class FollowMixin(models.Model):
"""
Each model that can be followed by a user must use this mixin.
"""
followers = GenericRelation('FollowStatus')
follower_count = models.IntegerField(default=0)

class Meta:
abstract = True


class LocationMixin(models.Model):
"""
Each model that contains a `Location` must use this mixin.
"""
location = GenericRelation('Location')

class Meta:
abstract = True


class MediaMixin(models.Model):
"""
Each model that contains `Media`s must use this mixin.
"""
medias = GenericRelation('Media')

class Meta:
abstract = True


class VoteMixin(models.Model):
"""
Each model that can be voted by a user must use this mixin.
"""
votes = GenericRelation('VoteStatus')
vote_count = models.IntegerField(default=0)

class Meta:
abstract = True

def update_vote_count(self, vote_value, voted_before):
if voted_before:
vote_value = vote_value * 2
self.vote_count = self.vote_count + vote_value
self.save()


class GenericModelMixin(models.Model):
"""
Allows generic relations between different models.
See https://docs.djangoproject.com/en/2.1/ref/contrib/contenttypes/
"""
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')

class Meta:
abstract = True


class Event(OwnerMixin, CommentMixin, FollowMixin, LocationMixin, MediaMixin, VoteMixin):
# Related fields
# TODO Decide if an artist must be a User in our system.
artists = models.ManyToManyField(settings.AUTH_USER_MODEL, db_table='event_artists',
related_name='performed_events', blank=True)
tags = models.ManyToManyField('Tag', db_table='event_tags', related_name='events',
blank=True)

# Own fields
title = models.CharField(max_length=100)
description = models.TextField()
date = models.DateTimeField(default=timezone.now)
price = models.DecimalField(max_digits=6, decimal_places=2, blank=True, default=0.0)
organizer_url = models.URLField(blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)


class AttendanceStatus(OwnerMixin):
ATTENDANCE_STATUS = (
('Y', 'Yes'),
('N', 'No'),
('M', 'Maybe'),
('A', 'Attended'),
('B', 'Blocked'),
)
event = models.ForeignKey(Event, related_name='attendance_status', on_delete=models.CASCADE)
status = models.CharField(max_length=1, choices=ATTENDANCE_STATUS)

class Meta:
# There can be only one attendance status between User and Event.
unique_together = ('owner', 'event')


class Comment(GenericModelMixin, OwnerMixin):
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)


class FollowStatus(GenericModelMixin, OwnerMixin):
class Meta:
# A user cannot follow the same item more than once
unique_together = ('owner', 'content_type', 'object_id')


class Location(GenericModelMixin, OwnerMixin):
# TODO Add required fields after doing research about Google Maps / Places API
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

class Meta:
# A model (User, Event etc.) cannot have more than one Location.
unique_together = ('content_type', 'object_id')


class Media(GenericModelMixin, OwnerMixin):
url = models.URLField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)


class Tag(models.Model):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should create a mixin for this too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a new PR later on.

name = models.CharField(max_length=20)


class VoteStatus(GenericModelMixin, OwnerMixin):
VOTE_CHOICES = (
('U', 'Up'),
('D', 'Down'),
)
vote = models.CharField(max_length=1, choices=VOTE_CHOICES)

class Meta:
# A user cannot vote for the same item more than once
unique_together = ('owner', 'content_type', 'object_id')

@property
def vote_value(self):
vote_value = 0
if self.vote == 'U':
vote_value = 1
elif self.vote == 'D':
vote_value = -1
return vote_value
9 changes: 9 additions & 0 deletions backend/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
Loading