diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 445c95a0ac76bd4c6ff30739ae2fb34a3e377a88..2158f0287c586a227a434cc11517580e25f80d29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,9 +17,9 @@ unit tests: script: - virtualenv -p python3 env - source ./env/bin/activate - - pip install -r requirements.txt - - pip install -r requirements-dev.txt - - pip install -r requirements-tests.txt + - pip install -U -r requirements.txt + - pip install -U -r requirements-dev.txt + - pip install -U -r requirements-tests.txt - echo "DEBUG = True" > ./project/settings/env.py - echo "SECRET_KEY = '$(pwgen 20 1)'" >> ./project/settings/env.py - ./manage.py migrate --run-syncdb diff --git a/apps/core/auth_backends.py b/apps/core/auth_backends.py index abe3208780be992ebf27ec2d075697a26293eee0..654e9c62c53d738e9deaa22386d25afa341df895 100644 --- a/apps/core/auth_backends.py +++ b/apps/core/auth_backends.py @@ -1,21 +1,36 @@ from django.contrib.auth.backends import ModelBackend -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model class EmailOrUsernameModelBackend(ModelBackend): """ - Authentification backend to allow email - overide AUTHENTICATION_BACKENDS with - fabauth.auth_backends.EmailOrUsernameModelBackend + We allow for email or username to be used as a login. + Case insensitive. """ - def authenticate(self, username=None, password=None, **kwargs): - if "@" in username: - kwargs = {"email": username} - else: - kwargs = {"username": username} + def authenticate(self, request, username=None, password=None): + # If we call this function, it means we're not authenticated yet. + # Since we're going through the classic Model backend first, it means + # that the user does not exists. + UserModel = get_user_model() + try: - user = User.objects.get(**kwargs) - if user.check_password(password): + user = UserModel.objects.get(email=username) + + if user.check_password(raw_password=password): return user - except User.DoesNotExist: + except UserModel.DoesNotExist: + # The User does not exist. Hashing the password anyway, + # to limit Time attacks efficiency + UserModel().set_password(raw_password=password) return None + + def get_user(self, user_id): + # This is used to get the User objects + UserModel = get_user_model() + print("get_user: {}".format(user_id)) + + try: + return UserModel.objects.get(pk=user_id) + except UserModel.DoesNotExist: + return None + diff --git a/apps/core/tests.py b/apps/core/tests.py index 1307aed959a780593e42b2ec8c2bfb28dc24ddb8..07d289617fad100dcd702daff179f35c1937cfc4 100644 --- a/apps/core/tests.py +++ b/apps/core/tests.py @@ -1,60 +1,58 @@ from django.test import TestCase -from django.test import Client +from django.test import Client, override_settings from django_factory_boy import auth as auth_factories -AUTHENTICATION_BACKENDS = [ + +@override_settings(AUTHENTICATION_BACKENDS=[ # Default "django.contrib.auth.backends.ModelBackend", # Email or Username for login "core.auth_backends.EmailOrUsernameModelBackend" -] - - + ]) class ApiTest(TestCase): def setUp(self): self.client = Client() self.user = auth_factories.UserFactory(password="dummypassword") def test_authenticate(self): - with self.settings(AUTHENTICATION_BACKENDS=AUTHENTICATION_BACKENDS): - # username, wrong password - login_status = self.client.login( - username=self.user.username, - password="notthepassword" - ) - assert not login_status - - # username, correct password - login_status = self.client.login( - username=self.user.username, - password="dummypassword" - ) - assert login_status - - # email, wrong password - login_status = self.client.login( - username=self.user.email, - password="notthepassowrd" - ) - assert not login_status - - # email, correct password - login_status = self.client.login( - username=self.user.email, - password="dummypassword" - ) - assert login_status - - # username, wrong user - login_status = self.client.login( - username="{}-2".format(self.user.username), - password="dummypassword" - ) - assert not login_status - - # email, wrong user - login_status = self.client.login( - username="{}-2".format(self.user.email), - password="dummypassword" - ) - assert not login_status + # username, wrong password + login_status = self.client.login( + username=self.user.username, + password="notthepassword" + ) + assert not login_status + + # username, correct password + login_status = self.client.login( + username=self.user.username, + password="dummypassword" + ) + assert login_status + + # email, wrong password + login_status = self.client.login( + username=self.user.email, + password="notthepassowrd" + ) + assert not login_status + + # email, correct password + login_status = self.client.login( + username=self.user.email, + password="dummypassword" + ) + assert login_status + + # username, wrong user + login_status = self.client.login( + username="{}-2".format(self.user.username), + password="dummypassword" + ) + assert not login_status + + # email, wrong user + login_status = self.client.login( + username="{}-2".format(self.user.email), + password="dummypassword" + ) + assert not login_status diff --git a/apps/rp/api/mixins.py b/apps/rp/api/mixins.py deleted file mode 100644 index 91646bde600b2d12827e851c063b6a795b353e5a..0000000000000000000000000000000000000000 --- a/apps/rp/api/mixins.py +++ /dev/null @@ -1,51 +0,0 @@ -from django_fsm import has_transition_perm, can_proceed -from rest_framework.decorators import detail_route -from rest_framework.exceptions import PermissionDenied -from rest_framework.response import Response - -import inspect - - -def get_transition_viewset_method(model, transition_name): - @detail_route(methods=['post']) - def inner_func(self, request, pk=None, *args, **kwargs): - object = self.get_object() - transition_method = getattr(object, transition_name) - - if not can_proceed(transition_method): - raise PermissionDenied - - if not has_transition_perm(transition_method, request.user): - raise PermissionDenied - - if 'by' in inspect.getargspec(transition_method).args: - transition_method(*args, by=request.user, **kwargs) - else: - transition_method(*args, **kwargs) - - if self.save_after_transition: - object.save() - - serializer = self.get_serializer(object) - return Response(serializer.data) - - return inner_func - - -def get_viewset_transition_actions_mixin(model): - """ - Automatically generate methods for Django REST Framework from transition - rules. - """ - instance = model() - - class Mixin(object): - save_after_transition = True - - transitions = instance.get_all_status_transitions() - transition_names = set(x.name for x in transitions) - for transition_name in transition_names: - setattr(Mixin, transition_name, - get_transition_viewset_method(model, transition_name)) - - return Mixin diff --git a/apps/rp/api/views.py b/apps/rp/api/views.py index e1172594ab1210f18cf5c89f8a3cdbdbf2c4c81e..44843f226e46d6c1a88ff9f98eec8bc7ddad9de5 100644 --- a/apps/rp/api/views.py +++ b/apps/rp/api/views.py @@ -1,13 +1,14 @@ from django.db.models import Q from rest_framework import viewsets, mixins +from djangorestframework_fsm.viewset_mixins import get_drf_fsm_mixin from rp.models import Article -from .serializers import ArticleSerializer from rp.views.articles import ArticleFilterMixin -from .mixins import get_viewset_transition_actions_mixin -ArticleMixin = get_viewset_transition_actions_mixin(Article) +from .serializers import ArticleSerializer + +ArticleMixin = get_drf_fsm_mixin(Article, fieldname='status') class ArticleViewSet(ArticleMixin, ArticleFilterMixin, viewsets.ModelViewSet): diff --git a/apps/rp/tests/test_article.py b/apps/rp/tests/test_article.py index 0afb5c36351ad02095de70b598d80b4423ba19bb..60cc6e76114c63620cf42899bc6410a0f032a39e 100644 --- a/apps/rp/tests/test_article.py +++ b/apps/rp/tests/test_article.py @@ -201,11 +201,14 @@ class TestArticleViews(TestCase): codename='can_edit')) self.client.force_login(user=self.user) - a = json.loads(serializers.serialize('json', [self.articles[0]]))[0] - a['fields']['title'] = 'Zog Zog' - r = self.client.post('/rp/article/edit/{}/'.format(a['pk']), a['fields']) - a = Article.objects.get(pk=a['pk']) - assert r.status_code == 302 # We're redirecting after edit + pk = self.articles[0].pk + a = {'title': 'Zog Zog', + 'status': 'NEW', + 'lang': 'FR', + 'url': 'https://www.example.org/'} + r = self.client.post('/rp/article/edit/{}/'.format(pk), a) + a = Article.objects.get(pk=pk) + assert r.status_code == 302 # We're redirecting to the view or preview of the article assert a.title == 'Zog Zog' @@ -243,8 +246,8 @@ class TestArticleApi(TestCase): article = {'title': 'Zog Zog', 'url': 'https://article.org/Zog+Zog'} r = self.client.put('/api/articles/{}/'.format(pk), - article, - format='json') + article, + format='json') assert r.status_code == 200 assert Article.objects.get(pk=pk).title == 'Zog Zog' @@ -265,7 +268,7 @@ class TestArticleApi(TestCase): assert r.status_code == 200 a = Article.objects.get(pk=pk) assert list(a.tags.values('name',).order_by('name')) == [{'name': 'New Tag 1'}, - {'name': 'New Tag 2'}] + {'name': 'New Tag 2'}] def test_api_filter_tag(self): tag = 'Tag 1' @@ -346,7 +349,7 @@ class TestArticleApi(TestCase): # We cannot recover a published article a = ArticleFactory(status='PUBLISHED') r = self.client.post('/api/articles/{}/recover/'.format(a.id)) - assert r.status_code == 403 + assert r.status_code == 400 def test_api_set_priority(self): self.user.user_permissions.add(Permission.objects.get( @@ -354,7 +357,7 @@ class TestArticleApi(TestCase): self.client.force_login(user=self.user) a = ArticleFactory(status='DRAFT') assert a.priority is False - r = self.client.post('/api/articles/{}/set_priority/'.format(a.id)) + r = self.client.post('/api/articles/{}/set-priority/'.format(a.id)) assert r.status_code == 200 assert r.data['priority'] @@ -364,6 +367,6 @@ class TestArticleApi(TestCase): self.client.force_login(user=self.user) a = ArticleFactory(status='DRAFT', priority=True) assert a.priority - r = self.client.post('/api/articles/{}/unset_priority/'.format(a.id)) + r = self.client.post('/api/articles/{}/unset-priority/'.format(a.id)) assert r.status_code == 200 assert r.data['priority'] is False diff --git a/project/urls.py b/project/urls.py index b2e7f7021ebed27999e4cf50942bc915ee826b4f..2e662d4b90a591f2e11050f392d87018f13e1f03 100644 --- a/project/urls.py +++ b/project/urls.py @@ -22,11 +22,11 @@ urlpatterns = [ url(r"^admin/", admin.site.urls), url(r'^i18n/', include('django.conf.urls.i18n')), - url(r"^api/", include(router.urls, namespace="api")), - url(r"^feeds/", include("rp.feeds.urls", namespace="feeds")), - url(r"^rp/", include("rp.urls", namespace="rp")), + url(r"^api/", include((router.urls, "api"))), + url(r"^feeds/", include(("rp.feeds.urls", "feeds"))), + url(r"^rp/", include(("rp.urls", "rp"))), url(r'^accounts/', include('allauth.urls')), - url(r"^users/", include('userprofile.urls', namespace="users")), + url(r"^users/", include(('userprofile.urls', "users"))), url(r"^docs/api/", include_docs_urls(title="API de la revue de presse")), ] diff --git a/requirements.txt b/requirements.txt index 0362834d31a89dbc7e0cb756e23e5614b71701e9..2bc2accd700e35eaf23bfebfc217f905eda08739 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,16 @@ -Django==1.11.2 -djangorestframework==3.6.3 -django-extensions==1.7.9 -django-imagekit==4.0 -django-taggit==0.22.0 +Django==2.2 +djangorestframework +django-extensions +django-imagekit +django-taggit django-taggit-serializer -Pillow==4.1.0 +Pillow selenium newspaper3k django-crispy-forms django-allauth django-fsm +djangorestframework-fsm django-markdown2 url coreapi