Feat urls (#257)

add new URLS, add swaggger doc, add tests
This commit is contained in:
Markos Gogoulos 2021-08-05 13:25:25 +03:00 committed by GitHub
parent 86cc0442d8
commit ba94989e6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 370 additions and 82 deletions

View File

@ -29,7 +29,7 @@ jobs:
shell: bash
- name: Run Django Tests
run: docker-compose -f docker-compose-dev.yaml exec -T web pytest
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
- name: Tear down the Stack
run: docker-compose -f docker-compose-dev.yaml down
run: docker-compose -f docker-compose-dev.yaml down

View File

@ -441,6 +441,13 @@ LOCAL_INSTALL = False
# it is placed here so it can be overrided on local_settings.py
GLOBAL_LOGIN_REQUIRED = False
# TODO: separate settings on production/development more properly, for now
# this should be ok
CELERY_TASK_ALWAYS_EAGER = False
if os.environ.get("TESTING"):
CELERY_TASK_ALWAYS_EAGER = True
try:
# keep a local_settings.py file for local overrides
from .local_settings import * # noqa

View File

@ -1,7 +1,7 @@
import debug_toolbar
from django.conf.urls import include, url
from django.conf.urls import include, re_path
from django.contrib import admin
from django.urls import path, re_path
from django.urls import path
from django.views.generic.base import TemplateView
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
@ -15,15 +15,15 @@ schema_view = get_schema_view(
urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)),
re_path(r"^__debug__/", include(debug_toolbar.urls)),
path(
"robots.txt",
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
),
url(r"^", include("files.urls")),
url(r"^", include("users.urls")),
url(r"^accounts/", include("allauth.urls")),
url(r"^api-auth/", include("rest_framework.urls")),
re_path(r"^", include("files.urls")),
re_path(r"^", include("users.urls")),
re_path(r"^accounts/", include("allauth.urls")),
re_path(r"^api-auth/", include("rest_framework.urls")),
path("admin/", admin.site.urls),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),

View File

1
files/tests/__init__.py Normal file
View File

@ -0,0 +1 @@
from .user_utils import create_account # noqa

24
files/tests/user_utils.py Normal file
View File

@ -0,0 +1,24 @@
from faker import Factory
from users.models import User
faker = Factory.create()
def create_account(username=None, email=None, password=None, name=None, **kwargs):
"Allow to create accounts by passing None or specific arguements"
email = email or faker.email()
username = username or email.split('a')[0]
password = password or faker.password()
name = name or faker.name()
description = kwargs.get('description') or faker.text()
is_superuser = kwargs.get('is_superuser') or False
is_manager = kwargs.get('is_manager') or False
is_editor = kwargs.get('is_editor') or False
user = User.objects.create(username=username, email=email, name=name, description=description, is_superuser=is_superuser, is_staff=is_superuser, is_editor=is_editor, is_manager=is_manager)
user.set_password(password)
user.save()
return user

View File

@ -1,5 +1,5 @@
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls import include, re_path
from django.conf.urls.static import static
from django.urls import path
@ -7,85 +7,85 @@ from . import management_views, views
from .feeds import IndexRSSFeed, SearchRSSFeed
urlpatterns = [
url(r"^$", views.index),
url(r"^about", views.about, name="about"),
url(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
url(r"^categories$", views.categories, name="categories"),
url(r"^contact$", views.contact, name="contact"),
url(r"^edit", views.edit_media, name="edit_media"),
url(r"^embed", views.embed_media, name="get_embed"),
url(r"^featured$", views.featured_media),
url(r"^fu/", include(("uploader.urls", "uploader"), namespace="uploader")),
url(r"^history$", views.history, name="history"),
url(r"^liked$", views.liked_media, name="liked_media"),
url(r"^latest$", views.latest_media),
url(r"^members", views.members, name="members"),
url(
re_path(r"^$", views.index),
re_path(r"^about", views.about, name="about"),
re_path(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
re_path(r"^categories$", views.categories, name="categories"),
re_path(r"^contact$", views.contact, name="contact"),
re_path(r"^edit", views.edit_media, name="edit_media"),
re_path(r"^embed", views.embed_media, name="get_embed"),
re_path(r"^featured$", views.featured_media),
re_path(r"^fu/", include(("uploader.urls", "uploader"), namespace="uploader")),
re_path(r"^history$", views.history, name="history"),
re_path(r"^liked$", views.liked_media, name="liked_media"),
re_path(r"^latest$", views.latest_media),
re_path(r"^members", views.members, name="members"),
re_path(
r"^playlist/(?P<friendly_token>[\w]*)$",
views.view_playlist,
name="get_playlist",
),
url(
re_path(
r"^playlists/(?P<friendly_token>[\w]*)$",
views.view_playlist,
name="get_playlist",
),
url(r"^popular$", views.recommended_media),
url(r"^recommended$", views.recommended_media),
re_path(r"^popular$", views.recommended_media),
re_path(r"^recommended$", views.recommended_media),
path("rss/", IndexRSSFeed()),
url("^rss/search", SearchRSSFeed()),
url(r"^search", views.search, name="search"),
url(r"^scpublisher", views.upload_media, name="upload_media"),
url(r"^tags", views.tags, name="tags"),
url(r"^tos$", views.tos, name="terms_of_service"),
url(r"^view", views.view_media, name="get_media"),
url(r"^upload", views.upload_media, name="upload_media"),
re_path("^rss/search", SearchRSSFeed()),
re_path(r"^search", views.search, name="search"),
re_path(r"^scpublisher", views.upload_media, name="upload_media"),
re_path(r"^tags", views.tags, name="tags"),
re_path(r"^tos$", views.tos, name="terms_of_service"),
re_path(r"^view", views.view_media, name="get_media"),
re_path(r"^upload", views.upload_media, name="upload_media"),
# API VIEWS
url(r"^api/v1/media$", views.MediaList.as_view()),
url(r"^api/v1/media/$", views.MediaList.as_view()),
url(
re_path(r"^api/v1/media$", views.MediaList.as_view()),
re_path(r"^api/v1/media/$", views.MediaList.as_view()),
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)$",
views.MediaDetail.as_view(),
name="api_get_media",
),
url(
re_path(
r"^api/v1/media/encoding/(?P<encoding_id>[\w]*)$",
views.EncodingDetail.as_view(),
name="api_get_encoding",
),
url(r"^api/v1/search$", views.MediaSearch.as_view()),
url(
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)/actions$",
views.MediaActions.as_view(),
),
url(r"^api/v1/categories$", views.CategoryList.as_view()),
url(r"^api/v1/tags$", views.TagList.as_view()),
url(r"^api/v1/comments$", views.CommentList.as_view()),
url(
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
re_path(r"^api/v1/tags$", views.TagList.as_view()),
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)/comments$",
views.CommentDetail.as_view(),
),
url(
re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)/comments/(?P<uid>[\w-]*)$",
views.CommentDetail.as_view(),
),
url(r"^api/v1/playlists$", views.PlaylistList.as_view()),
url(r"^api/v1/playlists/$", views.PlaylistList.as_view()),
url(
re_path(r"^api/v1/playlists$", views.PlaylistList.as_view()),
re_path(r"^api/v1/playlists/$", views.PlaylistList.as_view()),
re_path(
r"^api/v1/playlists/(?P<friendly_token>[\w]*)$",
views.PlaylistDetail.as_view(),
name="api_get_playlist",
),
url(r"^api/v1/user/action/(?P<action>[\w]*)$", views.UserActions.as_view()),
re_path(r"^api/v1/user/action/(?P<action>[\w]*)$", views.UserActions.as_view()),
# ADMIN VIEWS
url(r"^api/v1/encode_profiles/$", views.EncodeProfileList.as_view()),
url(r"^api/v1/manage_media$", management_views.MediaList.as_view()),
url(r"^api/v1/manage_comments$", management_views.CommentList.as_view()),
url(r"^api/v1/manage_users$", management_views.UserList.as_view()),
url(r"^api/v1/tasks$", views.TasksList.as_view()),
url(r"^api/v1/tasks/$", views.TasksList.as_view()),
url(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
url(r"^manage/comments$", views.manage_comments, name="manage_comments"),
url(r"^manage/media$", views.manage_media, name="manage_media"),
url(r"^manage/users$", views.manage_users, name="manage_users"),
re_path(r"^api/v1/encode_profiles/$", views.EncodeProfileList.as_view()),
re_path(r"^api/v1/manage_media$", management_views.MediaList.as_view()),
re_path(r"^api/v1/manage_comments$", management_views.CommentList.as_view()),
re_path(r"^api/v1/manage_users$", management_views.UserList.as_view()),
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -4,6 +4,8 @@ import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
os.environ.setdefault("TESTING", "True")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@ -0,0 +1,54 @@
from django.test import Client, TestCase
from rest_framework.authtoken.models import Token
from files.tests import create_account
API_V1_LOGIN_URL = '/api/v1/login'
class TestUserLogin(TestCase):
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
def setUp(self):
self.password = 'this_is_a_fake_password'
self.user = create_account(password=self.password)
def test_login_endpoint(self):
client = Client()
response = client.get(API_V1_LOGIN_URL)
self.assertEqual(
response.status_code,
405,
"GET not allowed here",
)
response = client.post(API_V1_LOGIN_URL, {'username': 'fake', 'password': 'fake'})
self.assertTrue('User not found' in str(response.content), 'Expected user not to be there')
user = self.user
response = client.post(API_V1_LOGIN_URL, {'username': user.username, 'password': self.password})
self.assertEqual(
response.status_code,
200,
"Expected 200",
)
data = response.data
self.assertEqual(
data.get('email'),
user.email,
"Expected user email",
)
self.assertEqual(
data.get('username'),
user.username,
"Expected username",
)
token = Token.objects.filter(user=user).first()
self.assertEqual(
data.get('token'),
token.key,
"Expected valid token",
)

View File

@ -0,0 +1,49 @@
from django.test import Client, TestCase
from rest_framework.authtoken.models import Token
from files.tests import create_account
API_V1_USER_TOKEN_URL = '/api/v1/user/token'
class TestUserToken(TestCase):
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
def setUp(self):
self.password = 'this_is_a_fake_password'
self.user = create_account(password=self.password)
def test_user_token_endpoint(self):
client = Client()
response = client.get(API_V1_USER_TOKEN_URL)
self.assertEqual(
response.status_code,
403,
"FORBIDDEN",
)
user = self.user
client.force_login(user=user)
response = client.post(API_V1_USER_TOKEN_URL)
self.assertEqual(
response.status_code,
405,
"method not allowed here",
)
response = client.get(API_V1_USER_TOKEN_URL)
data = response.data
self.assertEqual(
response.status_code,
200,
"expected 200",
)
token = Token.objects.filter(user=user).first()
self.assertEqual(
data.get('token'),
token.key,
"Expected valid token",
)

View File

@ -0,0 +1,41 @@
from django.test import Client, TestCase
from files.tests import create_account
API_V1_LOGIN_URL = '/api/v1/whoami'
class TestUserWhoami(TestCase):
fixtures = ["fixtures/categories.json", "fixtures/encoding_profiles.json"]
def setUp(self):
self.user = create_account()
def test_whoami_endpoint(self):
client = Client()
response = client.get(API_V1_LOGIN_URL)
self.assertEqual(
response.status_code,
403,
"Expected 403",
)
user = self.user
client.force_login(user=user)
response = client.get(API_V1_LOGIN_URL)
self.assertEqual(
response.status_code,
200,
"Expected 200",
)
data = response.data
self.assertEqual(
data.get('description'),
user.description,
"Expected user description",
)
self.assertEqual(
data.get('username'),
user.username,
"Expected username",
)

View File

@ -1,4 +0,0 @@
def test_new_user(user_factory):
print(user_factory.name)
print(user_factory.description)
assert True

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url
from django.conf.urls import re_path
from . import views
app_name = "uploader"
urlpatterns = [
url(r"^upload/$", views.FineUploaderView.as_view(), name="upload"),
re_path(r"^upload/$", views.FineUploaderView.as_view(), name="upload"),
]

View File

@ -1,5 +1,7 @@
from django import forms
from files.methods import is_mediacms_manager
from .models import Channel, User
@ -17,7 +19,6 @@ class UserForm(forms.ModelForm):
fields = (
"name",
"description",
"email",
"logo",
"notification_on_comments",
"is_featured",
@ -39,7 +40,7 @@ class UserForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
super(UserForm, self).__init__(*args, **kwargs)
self.fields.pop("is_featured")
if not user.is_superuser:
if not is_mediacms_manager(user):
self.fields.pop("advancedUser")
self.fields.pop("is_manager")
self.fields.pop("is_editor")

View File

@ -1,4 +1,7 @@
from django.conf import settings
from django.contrib.auth import authenticate
from rest_framework import serializers
from rest_framework.authtoken.models import Token
from .models import User
@ -77,3 +80,46 @@ class UserDetailSerializer(serializers.ModelSerializer):
"default_channel_edit_url",
)
extra_kwargs = {"name": {"required": False}}
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255, required=False)
username = serializers.CharField(max_length=255, required=False)
password = serializers.CharField(max_length=128, write_only=True)
token = serializers.CharField(max_length=255, required=False)
def validate(self, data):
email = data.get('email', None)
username = data.get('username', None)
password = data.get('password', None)
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'username' and not username:
raise serializers.ValidationError('username is required to log in.')
else:
username_or_email = username
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'email' and not email:
raise serializers.ValidationError('email is required to log in.')
else:
username_or_email = email
if settings.ACCOUNT_AUTHENTICATION_METHOD == 'username_email' and not (username or email):
raise serializers.ValidationError('username or email is required to log in.')
else:
username_or_email = username or email
if password is None:
raise serializers.ValidationError('password is required to log in.')
user = authenticate(username=username_or_email, password=password)
if user is None:
raise serializers.ValidationError('User not found.')
if not user.is_active:
raise serializers.ValidationError('User has been deactivated.')
token = Token.objects.filter(user=user).first()
if not token:
token = Token.objects.create(user=user)
return {'email': user.email, 'username': user.username, 'token': token.key}

View File

@ -1,41 +1,45 @@
from django.conf.urls import url
from django.conf.urls import re_path
from django.urls import path
from . import views
urlpatterns = [
url(r"^user/(?P<username>[\w@._-]*)$", views.view_user, name="get_user"),
url(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
url(
re_path(r"^user/(?P<username>[\w@._-]*)$", views.view_user, name="get_user"),
re_path(r"^user/(?P<username>[\w@._-]*)/$", views.view_user, name="get_user"),
re_path(
r"^user/(?P<username>[\w@.]*)/media$",
views.view_user_media,
name="get_user_media",
),
url(
re_path(
r"^user/(?P<username>[\w@.]*)/playlists$",
views.view_user_playlists,
name="get_user_playlists",
),
url(
re_path(
r"^user/(?P<username>[\w@.]*)/about$",
views.view_user_about,
name="get_user_about",
),
url(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
url(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
url(
re_path(r"^user/(?P<username>[\w@.]*)/edit$", views.edit_user, name="edit_user"),
re_path(r"^channel/(?P<friendly_token>[\w]*)$", views.view_channel, name="view_channel"),
re_path(
r"^channel/(?P<friendly_token>[\w]*)/edit$",
views.edit_channel,
name="edit_channel",
),
# API VIEWS
url(r"^api/v1/users$", views.UserList.as_view(), name="api_users"),
url(r"^api/v1/users/$", views.UserList.as_view()),
url(
path('api/v1/whoami', views.UserWhoami.as_view(), name='user-whoami'),
path('api/v1/user/token', views.UserToken.as_view(), name='user-token'),
path('api/v1/login', views.LoginView.as_view(), name='user-login'),
re_path(r"^api/v1/users$", views.UserList.as_view(), name="api_users"),
re_path(r"^api/v1/users/$", views.UserList.as_view()),
re_path(
r"^api/v1/users/(?P<username>[\w@._-]*)$",
views.UserDetail.as_view(),
name="api_get_user",
),
url(
re_path(
r"^api/v1/users/(?P<username>[\w@._-]*)/contact",
views.contact_user,
name="api_contact_user",

View File

@ -5,7 +5,8 @@ from django.http import HttpResponseRedirect
from django.shortcuts import render
from drf_yasg import openapi as openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status
from rest_framework import generics, permissions, status
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied
from rest_framework.parsers import (
@ -23,7 +24,7 @@ from files.methods import is_mediacms_editor, is_mediacms_manager
from .forms import ChannelForm, UserForm
from .models import Channel, User
from .serializers import UserDetailSerializer, UserSerializer
from .serializers import LoginSerializer, UserDetailSerializer, UserSerializer
def get_user(username):
@ -305,3 +306,65 @@ class UserDetail(APIView):
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class UserWhoami(generics.RetrieveAPIView):
parser_classes = (JSONParser, FormParser, MultiPartParser)
queryset = User.objects.all()
permission_classes = (permissions.IsAuthenticated,)
serializer_class = UserDetailSerializer
def get_object(self):
return User.objects.get(id=self.request.user.id)
@swagger_auto_schema(
tags=['Users'],
operation_summary='Whoami user information',
operation_description='Whoami user information',
responses={200: openapi.Response('response description', UserDetailSerializer), 403: 'Forbidden'},
)
def get(self, request, *args, **kwargs):
return super(UserWhoami, self).get(request, *args, **kwargs)
class UserToken(APIView):
parser_classes = (JSONParser,)
permission_classes = (permissions.IsAuthenticated,)
@swagger_auto_schema(
tags=['Users'],
operation_summary='Get a user token',
operation_description="Returns an authenticated user's token",
responses={200: 'token', 403: 'Forbidden'},
)
def get(self, request, *args, **kwargs):
token = Token.objects.filter(user=request.user).first()
if not token:
token = Token.objects.create(user=request.user)
return Response({'token': str(token)}, status=200)
class LoginView(APIView):
permission_classes = (permissions.AllowAny,)
serializer_class = LoginSerializer
parser_classes = (MultiPartParser, FormParser, FileUploadParser)
@swagger_auto_schema(
tags=['Users'],
operation_summary='Login url',
operation_description="Login url endpoint. According to what the portal provides, you may provide username and/or email, plus the password",
manual_parameters=[
openapi.Parameter(name="username", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="username"),
openapi.Parameter(name="email", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="email"),
openapi.Parameter(name="password", in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=True, description="password"),
],
responses={200: openapi.Response('user details', LoginSerializer), 404: 'Bad request'},
)
def post(self, request):
data = request.data
serializer = self.serializer_class(data=data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)