mirror of
https://github.com/pinry/pinry.git
synced 2026-01-16 12:12:06 +01:00
Merge pull request #323 from pinry/feature/api-support
Feature/api support
This commit is contained in:
@@ -12,7 +12,6 @@ For more information ( screenshots and document ) visit [getpinry.com](https://g
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Image fetch and online preview
|
||||
- Tagging system for Pins
|
||||
- Browser Extensions
|
||||
@@ -20,6 +19,7 @@ For more information ( screenshots and document ) visit [getpinry.com](https://g
|
||||
- Works well with docker
|
||||
- Both public and private boards (add @2020.02.11)
|
||||
- Search by tags / Search boards with name (add @2020.02.14)
|
||||
- Full API support via DRF (add @2022.02.19)
|
||||
|
||||
## Install with Docker
|
||||
See our full documentation at [https://docs.getpinry.com/install-with-docker/](https://docs.getpinry.com/install-with-docker/)
|
||||
|
||||
@@ -32,7 +32,7 @@ class ImageTests(APITestCase):
|
||||
data=data,
|
||||
format='json',
|
||||
)
|
||||
self.assertEqual(response.status_code, 403, response.data)
|
||||
self.assertEqual(response.status_code, 401, response.data)
|
||||
|
||||
|
||||
class BoardPrivacyTests(APITestCase):
|
||||
@@ -220,7 +220,7 @@ class PinTests(APITestCase):
|
||||
self.client.logout()
|
||||
uri = reverse("pin-detail", kwargs={"pk": pin.pk})
|
||||
response = self.client.patch(uri, format='json', data={})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 401, response.data)
|
||||
|
||||
def test_patch_detail(self):
|
||||
image = create_image()
|
||||
@@ -240,7 +240,8 @@ class PinTests(APITestCase):
|
||||
pin = create_pin(self.user, image, [])
|
||||
uri = reverse("pin-detail", kwargs={"pk": pin.pk})
|
||||
self.client.logout()
|
||||
self.assertEqual(self.client.delete(uri).status_code, 403)
|
||||
resp = self.client.delete(uri)
|
||||
self.assertEqual(resp.status_code, 401, resp.data)
|
||||
|
||||
def test_delete_detail(self):
|
||||
image = create_image()
|
||||
|
||||
30
docs/src/api.md
Normal file
30
docs/src/api.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# API Support
|
||||
|
||||
Pinry support API accessing via token so that you could write your own client or import any pin format to your instance.
|
||||
|
||||
# Get Token
|
||||
Just go to `My -> Profile` page to get the token.
|
||||
|
||||

|
||||
|
||||
# Access API via Token
|
||||
|
||||
Here is an example for curl to access `user-profile` API:
|
||||
|
||||
curl -X GET http://192.168.1.101:8080/api/v2/profile/users/ -H 'Authorization: Token fa3b0ed2b8a87c81323688c288642288c9570aca'
|
||||
|
||||
You will get response like this:
|
||||
|
||||
[
|
||||
{
|
||||
"username": "winkidney",
|
||||
"token": "fa3b0ed2b8a87c81323688c288642288c9570aca",
|
||||
"email": "winkidney@gmail.com",
|
||||
"gravatar": "0d7161ac663cdb21108502cd4051149c",
|
||||
"resource_link": "http://localhost:8000/api/v2/profile/users/1/"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# API reference
|
||||
Just use the interactive API interface by DRF or follow the file [api.js](https://github.com/pinry/pinry/blob/master/pinry-spa/src/components/api.js)
|
||||
BIN
docs/src/imgs/screenshots/token-access.png
Normal file
BIN
docs/src/imgs/screenshots/token-access.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -19,6 +19,7 @@ nav:
|
||||
- Theories: 'theories.md'
|
||||
- Install with Docker: 'install-with-docker.md'
|
||||
- Development: 'development.md'
|
||||
- API support: 'api.md'
|
||||
- Plugin System: 'plugin-system.md'
|
||||
- Upgrade Guide: 'upgrade-guide.md'
|
||||
- Docs: 'docs.md'
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
class="navbar-item">
|
||||
Pins
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'profile4user', params: {username: user.meta.username} }"
|
||||
class="navbar-item">
|
||||
Profile
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
<span>Boards</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="trueFalse2Class(inProfile)">
|
||||
<a @click="go2UserProfile">
|
||||
<b-icon
|
||||
type="is-dark"
|
||||
icon="account"
|
||||
custom-size="mdi-24px">
|
||||
</b-icon>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -71,6 +81,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inProfile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -91,6 +105,11 @@ export default {
|
||||
{ name: 'boards4user', params: { username: this.username } },
|
||||
);
|
||||
},
|
||||
go2UserProfile() {
|
||||
this.$router.push(
|
||||
{ name: 'profile4user', params: { username: this.username } },
|
||||
);
|
||||
},
|
||||
go2UserPins() {
|
||||
this.$router.push(
|
||||
{ name: 'user', params: { user: this.username } },
|
||||
|
||||
41
pinry-spa/src/components/user/profile.vue
Normal file
41
pinry-spa/src/components/user/profile.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="profile-container">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Token
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<p>Your Token is:</p>
|
||||
<pre>{{ token }}</pre>
|
||||
Please read <a target="_blank" href="https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication">DRF API Documentation</a> for more detail to know how to use it.
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'profile',
|
||||
props: ['token'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-container {
|
||||
margin-top: 2rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-shadow: 5px 5px 2px 1px rgba(0, 0, 255, .1);
|
||||
}
|
||||
|
||||
@import '../utils/grid-layout';
|
||||
@include screen-grid-layout(".profile-container");
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import Pins4User from '../views/Pins4User.vue';
|
||||
import Pins4Board from '../views/Pins4Board.vue';
|
||||
import Pins4Id from '../views/Pins4Id.vue';
|
||||
import Boards4User from '../views/Boards4User.vue';
|
||||
import Profile4User from '../views/Profile4User.vue';
|
||||
import PinCreate from '../views/PinCreate.vue';
|
||||
import Search from '../views/Search.vue';
|
||||
import PageNotFound from '../views/PageNotFound.vue';
|
||||
@@ -43,6 +44,11 @@ const routes = [
|
||||
name: 'boards4user',
|
||||
component: Boards4User,
|
||||
},
|
||||
{
|
||||
path: '/profile/:username',
|
||||
name: 'profile4user',
|
||||
component: Profile4User,
|
||||
},
|
||||
{
|
||||
path: '/pin-creation/from-url',
|
||||
name: 'pin-creation-from-url',
|
||||
|
||||
54
pinry-spa/src/views/Profile4User.vue
Normal file
54
pinry-spa/src/views/Profile4User.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="profile-for-user">
|
||||
<PHeader></PHeader>
|
||||
<UserProfileCard :in-profile="true" :username="filters.userFilter"></UserProfileCard>
|
||||
<Profile :token="profile.token"></Profile>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PHeader from '../components/PHeader.vue';
|
||||
import UserProfileCard from '../components/UserProfileCard.vue';
|
||||
import Profile from '../components/user/profile.vue';
|
||||
import api from '../components/api';
|
||||
|
||||
export default {
|
||||
name: 'Profile4User',
|
||||
data() {
|
||||
return {
|
||||
filters: { userFilter: null },
|
||||
profile: {},
|
||||
};
|
||||
},
|
||||
components: {
|
||||
PHeader,
|
||||
UserProfileCard,
|
||||
Profile,
|
||||
},
|
||||
created() {
|
||||
this.initializeBoard();
|
||||
this.initializeUser();
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.filters = { userFilter: to.params.username };
|
||||
next();
|
||||
},
|
||||
methods: {
|
||||
initializeBoard() {
|
||||
this.filters = { userFilter: this.$route.params.username };
|
||||
},
|
||||
initializeUser() {
|
||||
const self = this;
|
||||
api.User.fetchUserInfo(false).then(
|
||||
(user) => {
|
||||
self.profile = user;
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'django_filters',
|
||||
'taggit',
|
||||
'compressor',
|
||||
@@ -163,4 +164,9 @@ REST_FRAMEWORK = {
|
||||
'URL_FIELD_NAME': DRF_URL_FIELD_NAME,
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'PAGE_SIZE': API_LIMIT_PER_PAGE,
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import hashlib
|
||||
|
||||
from django.contrib.auth.models import User as BaseUser
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
||||
def create_token_if_necessary(user: BaseUser):
|
||||
from rest_framework.authtoken.models import Token
|
||||
token = Token.objects.filter(user=user).first()
|
||||
if token is not None:
|
||||
return token
|
||||
else:
|
||||
return Token.objects.create(user=user)
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
|
||||
@property
|
||||
def gravatar(self):
|
||||
return hashlib.md5(self.email.encode('utf-8')).hexdigest()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_profile(sender, instance: User, **kwargs):
|
||||
create_token_if_necessary(instance)
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.contrib.auth import login
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from users.models import User
|
||||
from users.models import User, create_token_if_necessary
|
||||
|
||||
|
||||
class PublicUserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
@@ -26,6 +26,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
model = User
|
||||
fields = (
|
||||
'username',
|
||||
'token',
|
||||
'email',
|
||||
'gravatar',
|
||||
'password',
|
||||
@@ -52,6 +53,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
min_length=6,
|
||||
max_length=32,
|
||||
)
|
||||
token = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
if validated_data['password'] != validated_data['password_repeat']:
|
||||
@@ -73,3 +75,6 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
backend=settings.AUTHENTICATION_BACKENDS[0],
|
||||
)
|
||||
return user
|
||||
|
||||
def get_token(self, obj: User):
|
||||
return create_token_if_necessary(obj).key
|
||||
|
||||
@@ -61,7 +61,7 @@ class CreateUserTest(TestCase):
|
||||
reverse('users:user-list'),
|
||||
data=data,
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
|
||||
class LogoutViewTest(TestCase):
|
||||
|
||||
Reference in New Issue
Block a user