Merge pull request #178 from pinry/feature/private-pin-and-board

close  #52 private pin and board
This commit is contained in:
Ji Qu
2020-02-11 17:59:09 +08:00
committed by GitHub
12 changed files with 261 additions and 24 deletions

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2020-02-11 05:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_remove_pin_origin'),
]
operations = [
migrations.AddField(
model_name='pin',
name='private',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2020-02-11 08:38
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_pin_private'),
]
operations = [
migrations.AddField(
model_name='board',
name='private',
field=models.BooleanField(default=False),
),
]

View File

@@ -76,6 +76,7 @@ class Board(models.Model):
submitter = models.ForeignKey(User)
name = models.CharField(max_length=128, blank=False, null=False)
private = models.BooleanField(default=False, blank=False)
pins = models.ManyToManyField("Pin", related_name="pins", blank=True)
published = models.DateTimeField(auto_now_add=True)
@@ -83,6 +84,7 @@ class Board(models.Model):
class Pin(models.Model):
submitter = models.ForeignKey(User)
private = models.BooleanField(default=False, blank=False)
url = models.CharField(null=True, blank=True, max_length=256)
referer = models.CharField(null=True, blank=True, max_length=256)
description = models.TextField(blank=True, null=True)

View File

@@ -21,6 +21,19 @@ class IsOwnerOrReadOnly(permissions.IsAuthenticatedOrReadOnly):
return getattr(obj, self.__owner_field_name) == request.user
class OwnerOnlyIfPrivate(permissions.BasePermission):
def __init__(self, owner_field_name="owner"):
self.__owner_field_name = owner_field_name
def __call__(self):
return self
def has_object_permission(self, request, view, obj):
if getattr(obj, "private"):
return request.user == getattr(obj, self.__owner_field_name)
return True
class OwnerOnly(permissions.IsAuthenticatedOrReadOnly):
def has_permission(self, request, view):

View File

@@ -1,4 +1,5 @@
from django.conf import settings
from django.db.models import Q
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from taggit.models import Tag
@@ -9,6 +10,22 @@ from django_images.models import Thumbnail
from users.serializers import UserSerializer
def filter_private_pin(request, query):
if request.user.is_authenticated:
query = query.exclude(~Q(submitter=request.user), private=True)
else:
query = query.exclude(private=True)
return query.select_related('image', 'submitter')
def filter_private_board(request, query):
if request.user.is_authenticated:
query = query.exclude(~Q(submitter=request.user), private=True)
else:
query = query.exclude(private=True)
return query
class ThumbnailSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Thumbnail
@@ -72,6 +89,7 @@ class PinSerializer(serializers.HyperlinkedModelSerializer):
model = Pin
fields = (
settings.DRF_URL_FIELD_NAME,
"private",
"id",
"submitter",
"url",
@@ -151,6 +169,7 @@ class BoardSerializer(serializers.HyperlinkedModelSerializer):
settings.DRF_URL_FIELD_NAME,
"id",
"name",
"private",
"pins",
"pins_detail",
"published",
@@ -164,7 +183,9 @@ class BoardSerializer(serializers.HyperlinkedModelSerializer):
}
submitter = UserSerializer(read_only=True)
pins_detail = PinSerializer(source="pins", many=True, read_only=True)
pins_detail = serializers.SerializerMethodField(
read_only=True,
)
pins = serializers.HyperlinkedRelatedField(
write_only=True,
queryset=Pin.objects.all(),
@@ -187,6 +208,12 @@ class BoardSerializer(serializers.HyperlinkedModelSerializer):
help_text="only patch method works for this field"
)
def get_pins_detail(self, instance):
query = instance.pins.all()
request = self.context['request']
query = filter_private_pin(request, query)
return [PinSerializer(pin, context=self.context).data for pin in query]
@staticmethod
def _get_list(pins_id):
return tuple(Pin.objects.filter(id__in=pins_id))
@@ -194,10 +221,11 @@ class BoardSerializer(serializers.HyperlinkedModelSerializer):
def update(self, instance: Board, validated_data):
pins_to_add = validated_data.pop("pins_to_add", [])
pins_to_remove = validated_data.pop("pins_to_remove", [])
if Board.objects.filter(
board = Board.objects.filter(
submitter=instance.submitter,
name=validated_data.get('name', None)
).exists():
).first()
if board.id != instance.id:
raise ValidationError(
detail={'name': "Board with this name already exists"}
)

View File

@@ -5,11 +5,17 @@ import mock
from rest_framework import status
from rest_framework.test import APITestCase
from django_images.models import Thumbnail
from taggit.models import Tag
from .helpers import create_image, create_user, create_pin
from core.models import Pin, Image
from core.models import Pin, Image, Board
def _teardown_models():
Pin.objects.all().delete()
Image.objects.all().delete()
Tag.objects.all().delete()
Board.objects.all().delete()
def mock_requests_get(url, **kwargs):
@@ -29,6 +35,117 @@ class ImageTests(APITestCase):
self.assertEqual(response.status_code, 403, response.data)
class BoardPrivacyTests(APITestCase):
def setUp(self):
super(BoardPrivacyTests, self).setUp()
self.owner = create_user("default")
self.non_owner = create_user("non_owner")
self.private_board = Board.objects.create(
name="test_board",
submitter=self.owner,
private=True,
)
self.board_url = reverse("board-detail", kwargs={"pk": self.private_board.pk})
self.boards_url = reverse("board-list")
def tearDown(self):
_teardown_models()
def test_should_non_owner_and_anonymous_user_has_no_permission_to_list_private_board(self):
resp = self.client.get(self.boards_url)
self.assertEqual(len(resp.data), 0, resp.data)
self.client.login(username=self.non_owner.username, password='password')
resp = self.client.get(self.boards_url)
self.assertEqual(len(resp.data), 0, resp.data)
def test_should_owner_has_permission_to_list_private_board(self):
self.client.login(username=self.non_owner.username, password='password')
resp = self.client.get(self.boards_url)
self.assertEqual(len(resp.data), 0, resp.data)
def test_should_non_owner_and_anonymous_user_has_no_permission_to_view_private_board(self):
resp = self.client.get(self.board_url)
self.assertEqual(resp.status_code, 404)
self.client.login(username=self.non_owner.username, password='password')
resp = self.client.get(self.board_url)
self.assertEqual(resp.status_code, 404)
def test_should_owner_has_permission_to_view_private_board(self):
self.client.login(username=self.owner.username, password='password')
resp = self.client.get(self.board_url)
self.assertEqual(resp.status_code, 200)
class PinPrivacyTests(APITestCase):
def setUp(self):
super(PinPrivacyTests, self).setUp()
self.owner = create_user("default")
self.non_owner = create_user("non_owner")
with mock.patch('requests.get', mock_requests_get):
image = Image.objects.create_for_url('http://a.com/b.png')
self.private_pin = Pin.objects.create(
submitter=self.owner,
image=image,
private=True,
)
self.private_pin_url = reverse("pin-detail", kwargs={"pk": self.private_pin.pk})
self.board = Board.objects.create(name="test_board", submitter=self.owner)
self.board.pins.add(self.private_pin)
self.board.save()
self.board_url = reverse("board-detail", kwargs={"pk": self.board.pk})
def tearDown(self):
_teardown_models()
def test_should_non_owner_and_anonymous_user_has_no_permission_to_list_private_pin(self):
resp = self.client.get(reverse("pin-list"))
self.assertEqual(len(resp.data['results']), 0, resp.data)
self.client.login(username=self.non_owner.username, password='password')
resp = self.client.get(reverse("pin-list"))
self.assertEqual(len(resp.data['results']), 0, resp.data)
def test_should_non_owner_and_anonymous_user_has_no_permission_to_list_private_pin_in_board(self):
resp = self.client.get(self.board_url)
self.assertEqual(len(resp.data['pins_detail']), 0, resp.data)
self.client.login(username=self.non_owner.username, password='password')
resp = self.client.get(self.board_url)
self.assertEqual(len(resp.data['pins_detail']), 0, resp.data)
def test_should_owner_user_has_permission_to_list_private_pin_in_board(self):
self.client.login(username=self.owner.username, password='password')
resp = self.client.get(self.board_url)
self.assertEqual(len(resp.data['pins_detail']), 1, resp.data)
def test_should_owner_user_has_permission_to_list_private_pin(self):
self.client.login(username=self.owner.username, password='password')
resp = self.client.get(reverse("pin-list"))
self.assertEqual(len(resp.data['results']), 1, resp.data)
def test_should_owner_has_permission_to_view_private_pin(self):
self.client.login(username=self.owner.username, password='password')
resp = self.client.get(self.private_pin_url)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['id'], self.private_pin.id)
def test_should_anonymous_user_has_no_permission_to_view_private_pin(self):
resp = self.client.get(self.private_pin_url)
self.assertEqual(resp.status_code, 404)
def test_should_non_owner_has_no_permission_to_view_private_pin(self):
self.client.login(username=self.non_owner.username, password='password')
resp = self.client.get(self.private_pin_url)
self.assertEqual(resp.status_code, 404)
class PinTests(APITestCase):
_JSON_TYPE = "application/json"
@@ -38,9 +155,7 @@ class PinTests(APITestCase):
self.client.login(username=self.user.username, password='password')
def tearDown(self):
Pin.objects.all().delete()
Image.objects.all().delete()
Tag.objects.all().delete()
_teardown_models()
@mock.patch('requests.get', mock_requests_get)
def test_should_create_pin(self):
@@ -49,6 +164,7 @@ class PinTests(APITestCase):
referer = 'http://testserver.com/'
post_data = {
'url': url,
'private': False,
'referer': referer,
'description': 'That\'s an Apple!'
}

View File

@@ -8,7 +8,8 @@ from taggit.models import Tag
from core import serializers as api
from core.models import Image, Pin, Board
from core.permissions import IsOwnerOrReadOnly
from core.permissions import IsOwnerOrReadOnly, OwnerOnlyIfPrivate
from core.serializers import filter_private_pin, filter_private_board
class ImageViewSet(mixins.CreateModelMixin, GenericViewSet):
@@ -20,36 +21,45 @@ class ImageViewSet(mixins.CreateModelMixin, GenericViewSet):
class PinViewSet(viewsets.ModelViewSet):
queryset = Pin.objects.all().select_related('image', 'submitter')
serializer_class = api.PinSerializer
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filter_fields = ("submitter__username", 'tags__name', )
ordering_fields = ('-id', )
ordering = ('-id', )
permission_classes = [IsOwnerOrReadOnly("submitter"), ]
permission_classes = [IsOwnerOrReadOnly("submitter"), OwnerOnlyIfPrivate("submitter")]
def get_queryset(self):
query = Pin.objects.all()
request = self.request
return filter_private_pin(request, query)
class BoardViewSet(viewsets.ModelViewSet):
queryset = Board.objects.all()
serializer_class = api.BoardSerializer
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_fields = ("submitter__username", )
ordering_fields = ('-id', )
ordering = ('-id', )
permission_classes = [IsOwnerOrReadOnly("submitter"), ]
permission_classes = [IsOwnerOrReadOnly("submitter"), OwnerOnlyIfPrivate("submitter")]
def get_queryset(self):
return filter_private_board(self.request, Board.objects.all())
class BoardAutoCompleteViewSet(
mixins.ListModelMixin,
viewsets.GenericViewSet,
):
queryset = Board.objects.all()
serializer_class = api.BoardAutoCompleteSerializer
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_fields = ("submitter__username", )
ordering_fields = ('-id', )
ordering = ('-id', )
pagination_class = None
permission_classes = [OwnerOnlyIfPrivate("submitter"), ]
def get_queryset(self):
return filter_private_board(self.request, Board.objects.all())
class TagAutoCompleteViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
@@ -67,8 +77,8 @@ class TagAutoCompleteViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
drf_router = routers.DefaultRouter()
drf_router.register(r'pins', PinViewSet)
drf_router.register(r'pins', PinViewSet, basename="pin")
drf_router.register(r'images', ImageViewSet)
drf_router.register(r'boards', BoardViewSet)
drf_router.register(r'boards', BoardViewSet, basename="board")
drf_router.register(r'tags-auto-complete', TagAutoCompleteViewSet)
drf_router.register(r'boards-auto-complete', BoardAutoCompleteViewSet)
drf_router.register(r'boards-auto-complete', BoardAutoCompleteViewSet, base_name="board")

View File

@@ -17,6 +17,13 @@
maxlength="128"
>
</b-input>
</b-field>
<b-field label="Privacy Option"
:type="createModel.form.private.type"
:message="createModel.form.private.error">
<b-checkbox v-model="createModel.form.private.value">
{{ createModel.form.private.value?"only visible to yourself":"visible to everyone" }}
</b-checkbox>
</b-field>
</div>
<div v-if="isEdit">
@@ -30,6 +37,13 @@
maxlength="128"
>
</b-input>
</b-field>
<b-field label="Privacy Option"
:type="editModel.form.private.type"
:message="editModel.form.private.error">
<b-checkbox v-model="editModel.form.private.value">
{{ editModel.form.private.value?"only visible to yourself":"visible to everyone" }}
</b-checkbox>
</b-field>
</div>
</section>
@@ -56,7 +70,7 @@ import API from './api';
import ModelForm from './utils/ModelForm';
import bus from './utils/bus';
const fields = ['name'];
const fields = ['name', 'private'];
export default {
name: 'BoardEditModal',
@@ -108,7 +122,10 @@ export default {
},
createBoard() {
const self = this;
const promise = API.Board.create(this.createModel.form.name.value);
const promise = API.Board.create(
this.createModel.form.name.value,
this.createModel.form.private.value,
);
promise.then(
(resp) => {
bus.bus.$emit(bus.events.refreshBoards);

View File

@@ -76,6 +76,7 @@ function createBoardItem(board) {
}
boardItem.id = board.id;
boardItem.name = board.name;
boardItem.private = board.private;
boardItem.total_pins = pins4Board.length;
if (previewImage.image.thumbnail.image !== null) {
boardItem.preview_image_url = pinHandler.escapeUrl(

View File

@@ -90,6 +90,7 @@ function createImageItem(pin) {
image.url = pinHandler.escapeUrl(pin.image.thumbnail.image);
image.id = pin.id;
image.owner_id = pin.submitter.id;
image.private = pin.private;
image.description = pin.description;
image.tags = pin.tags;
image.author = pin.submitter.username;

View File

@@ -4,9 +4,9 @@ import storage from './utils/storage';
const API_PREFIX = '/api/v2/';
const Board = {
create(name) {
create(name, private_ = false) {
const url = `${API_PREFIX}boards/`;
const data = { name };
const data = { name, private: private_ };
return new Promise(
(resolve, reject) => {
axios.post(url, data).then(

View File

@@ -27,6 +27,13 @@
>
</b-input>
</b-field>
<b-field label="Privacy Option"
:type="pinModel.form.private.type"
:message="pinModel.form.private.error">
<b-checkbox v-model="pinModel.form.private.value">
{{ pinModel.form.private.value?"only visible to yourself":"visible to everyone" }}
</b-checkbox>
</b-field>
<b-field label="Image Referer"
:type="pinModel.form.referer.type"
:message="pinModel.form.referer.error">
@@ -107,7 +114,7 @@ function isURLBlank(url) {
return url !== null && url === '';
}
const fields = ['url', 'referer', 'description', 'tags'];
const fields = ['url', 'referer', 'description', 'tags', 'private'];
export default {
name: 'PinCreateModal',
@@ -160,11 +167,13 @@ export default {
this.pinModel.form.referer.value = this.existedPin.referer;
this.pinModel.form.description.value = this.existedPin.description;
this.pinModel.form.tags.value = this.existedPin.tags;
this.pinModel.form.private.value = this.existedPin.private;
}
if (this.fromUrl) {
this.pinModel.form.url.value = this.fromUrl.url;
this.pinModel.form.referer.value = this.fromUrl.referer;
this.pinModel.form.description.value = this.fromUrl.description;
this.pinModel.form.private.value = false;
}
},
methods: {
@@ -216,7 +225,7 @@ export default {
savePin() {
const self = this;
const data = this.pinModel.asDataByFields(
['referer', 'description', 'tags'],
['referer', 'description', 'tags', 'private'],
);
const promise = API.Pin.updateById(this.existedPin.id, data);
promise.then(
@@ -239,7 +248,7 @@ export default {
promise = API.Pin.createFromURL(data);
} else {
const data = this.pinModel.asDataByFields(
['referer', 'description', 'tags'],
['referer', 'description', 'tags', 'private'],
);
data.image_by_id = this.formUpload.imageId;
promise = API.Pin.createFromUploaded(data);