Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
La Quadrature du Net
rpteam
Revue de Press
Commits
872a80d1
Commit
872a80d1
authored
May 02, 2017
by
cynddl
Browse files
Add initial authentication and authorization for views and API
parent
5ab9a7df
Changes
11
Hide whitespace changes
Inline
Side-by-side
apps/core/management/commands/init_groups.py
View file @
872a80d1
...
...
@@ -3,13 +3,20 @@ from django.contrib.auth.models import Group, Permission
groups
=
[
"jedi"
,
"padawan"
]
permissions
=
{
"jedi"
:
[],
"padawans"
:
[]
"jedi"
:
[
"can_change_status"
,
"can_change_priority"
,
"can_vote"
,
"can_edit"
],
"padawan"
:
[
"can_vote"
,
"add_article"
]
}
class
Command
(
BaseCommand
):
help
=
"Adds initial groups for the application (jedi and padawans)"
help
=
"Adds initial groups for the application (jedi
s
and padawans)"
def
handle
(
self
,
*
args
,
**
options
):
pass
for
g
in
groups
:
print
(
"Creating group '{}'"
.
format
(
g
))
new_group
,
created
=
Group
.
objects
.
get_or_create
(
name
=
g
)
for
p
in
permissions
[
g
]:
new_group
.
permissions
.
add
(
Permission
.
objects
.
get
(
codename
=
p
))
apps/rp/api/mixins.py
0 → 100644
View file @
872a80d1
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
apps/rp/api/views.py
View file @
872a80d1
from
rest_framework
import
viewsets
from
rest_framework.decorators
import
detail_route
from
rest_framework.response
import
Response
from
rp.models
import
Article
from
.serializers
import
ArticleSerializer
from
.mixins
import
get_viewset_transition_actions_mixin
ArticleMixin
=
get_viewset_transition_actions_mixin
(
Article
)
class
ArticleViewSet
(
viewsets
.
ModelViewSet
):
class
ArticleViewSet
(
ArticleMixin
,
viewsets
.
ModelViewSet
):
queryset
=
Article
.
objects
.
all
()
serializer_class
=
ArticleSerializer
def
response_serialized_object
(
self
,
object
):
return
Response
(
self
.
serializer_class
(
object
).
data
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"publish"
)
def
publish
(
self
,
request
,
pk
=
None
):
article
=
self
.
get_object
()
article
.
publish
()
article
.
save
()
return
self
.
response_serialized_object
(
article
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"reject"
)
def
reject
(
self
,
request
,
pk
=
None
):
article
=
self
.
get_object
()
article
.
reject
()
article
.
save
()
return
self
.
response_serialized_object
(
article
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"recover"
)
def
recover
(
self
,
request
,
pk
=
None
):
article
=
self
.
get_object
()
article
.
recover
()
article
.
save
()
return
self
.
response_serialized_object
(
article
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"upvote"
)
def
upvote
(
self
,
request
,
pk
=
None
):
article
=
self
.
get_object
()
article
.
upvote
(
user_object
=
request
.
user
.
username
)
return
self
.
response_serialized_object
(
article
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"downvote"
)
def
downvote
(
self
,
request
,
pk
=
None
):
article
=
self
.
get_object
()
article
.
downvote
(
user_object
=
request
.
user
.
username
)
return
self
.
response_serialized_object
(
article
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"priority/on"
)
def
priority_on
(
self
,
request
,
pk
=
None
,
priority
=
True
):
article
=
self
.
get_object
()
article
.
set_priority
(
True
)
article
.
save
()
return
self
.
response_serialized_object
(
article
)
@
detail_route
(
methods
=
[
"post"
],
url_path
=
"priority/off"
)
def
priority_off
(
self
,
request
,
pk
=
None
,
priority
=
True
):
article
=
self
.
get_object
()
article
.
set_priority
(
False
)
article
.
save
()
return
self
.
response_serialized_object
(
article
)
apps/rp/migrations/0014_auto_20170501_1611.py
0 → 100644
View file @
872a80d1
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-01 16:11
from
__future__
import
unicode_literals
from
django.db
import
migrations
import
django_fsm
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'rp'
,
'0013_auto_20170428_1341'
),
]
operations
=
[
migrations
.
AlterModelOptions
(
name
=
'article'
,
options
=
{
'permissions'
:
((
'can_change_status'
,
'Can change article status'
),
(
'can_change_priority'
,
'Can change article priority'
),
(
'can_vote'
,
'Can vote articles'
),
(
'can_edit'
,
'Can edit articles'
)),
'verbose_name'
:
'Article'
,
'verbose_name_plural'
:
'Articles'
},
),
migrations
.
AlterField
(
model_name
=
'article'
,
name
=
'status'
,
field
=
django_fsm
.
FSMField
(
choices
=
[(
'NEW'
,
'New'
),
(
'DRAFT'
,
'Draft'
),
(
'PUBLISHED'
,
'Published'
),
(
'REJECTED'
,
'Rejected'
)],
default
=
'NEW'
,
max_length
=
50
,
protected
=
True
),
),
]
apps/rp/models/article.py
View file @
872a80d1
...
...
@@ -75,6 +75,7 @@ class Article(VoteMixin):
(
"can_change_status"
,
"Can change article status"
),
(
"can_change_priority"
,
"Can change article priority"
),
(
"can_vote"
,
"Can vote articles"
),
(
"can_edit"
,
"Can edit articles"
)
)
def
__str__
(
self
):
...
...
@@ -82,7 +83,8 @@ class Article(VoteMixin):
# Finite state logic
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'PUBLISHED'
)
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'PUBLISHED'
,
permission
=
"can_change_status"
)
def
publish
(
self
):
self
.
published_at
=
datetime
.
now
()
...
...
@@ -98,14 +100,19 @@ class Article(VoteMixin):
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
permission
=
"can_change_priority"
)
def
set_priority
(
self
,
value
):
self
.
priority
=
value
def
set_priority
(
self
):
self
.
priority
=
True
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
permission
=
"can_change_priority"
)
def
unset_priority
(
self
):
self
.
priority
=
False
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
)
@
transition
(
field
=
status
,
source
=
'NEW'
,
target
=
RETURN_VALUE
(
'NEW'
,
'DRAFT'
),
permission
=
"can_vote"
)
def
upvote
(
self
,
user_object
):
super
(
Article
,
self
).
upvote
(
user_object
)
def
upvote
(
self
,
by
=
None
):
super
(
Article
,
self
).
upvote
(
by
)
if
self
.
und_score
>=
ARTICLE_SCORE_THRESHOLD
:
return
'DRAFT'
else
:
...
...
@@ -114,8 +121,8 @@ class Article(VoteMixin):
@
transition
(
field
=
status
,
source
=
'NEW'
,
target
=
'NEW'
,
permission
=
"can_vote"
)
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
permission
=
"can_vote"
)
def
downvote
(
self
,
user_object
):
super
(
Article
,
self
).
downvote
(
user_object
)
def
downvote
(
self
,
by
=
None
):
super
(
Article
,
self
).
downvote
(
by
)
# Content extraction
...
...
apps/rp/templates/rp/article_list.html
View file @
872a80d1
...
...
@@ -218,8 +218,13 @@
}
function
call_priority
(
id
,
flag
)
{
var
url
=
"
/api/articles/
"
+
id
+
"
/priority/
"
+
(
flag
?
"
on/
"
:
"
off/
"
);
$
.
post
(
url
,
{
'
priority
'
:
flag
},
function
response
(
data
)
{
if
(
flag
)
{
var
url
=
"
/api/articles/
"
+
id
+
"
/set_priority/
"
;
}
else
{
var
url
=
"
/api/articles/
"
+
id
+
"
/unset_priority/
"
;
}
$
.
post
(
url
,
function
response
(
data
)
{
$
(
"
#priority_
"
+
id
).
toggleClass
(
"
fa-star
"
).
toggleClass
(
"
fa-star-o
"
);
});
}
...
...
apps/rp/urls.py
View file @
872a80d1
from
django.contrib.auth.decorators
import
login_required
from
django.conf.urls
import
url
from
rp.views.articles
import
ArticleListFlux
,
ArticleEdit
,
ArticleDetailView
urlpatterns
=
[
url
(
r
"^article/list/(?P<filter_view>\w+)"
,
ArticleListFlux
.
as_view
(),
login_required
(
ArticleListFlux
.
as_view
()
)
,
name
=
"article-list"
),
url
(
r
"^article/list"
,
ArticleListFlux
.
as_view
(),
login_required
(
ArticleListFlux
.
as_view
()
)
,
name
=
"article-list"
),
url
(
r
"^article/edit/(?P<pk>\d+)"
,
ArticleEdit
.
as_view
(),
login_required
(
ArticleEdit
.
as_view
()
)
,
name
=
"article-edit"
),
url
(
r
"^article/view/(?P<pk>\d+)"
,
ArticleDetailView
.
as_view
(),
login_required
(
ArticleDetailView
.
as_view
()
)
,
name
=
"article-view"
),
url
(
r
"^article/preview/(?P<pk>\d+)"
,
ArticleDetailView
.
as_view
(
preview
=
True
),
login_required
(
ArticleDetailView
.
as_view
(
preview
=
True
)
)
,
name
=
"article-preview"
)
]
apps/rp/views/articles.py
View file @
872a80d1
...
...
@@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _
from
django.urls
import
reverse
,
reverse_lazy
from
django
import
forms
from
django.contrib.auth.mixins
import
LoginRequiredMixin
,
PermissionRequiredMixin
from
crispy_forms.helper
import
FormHelper
from
crispy_forms.layout
import
Layout
,
Field
,
Div
,
HTML
from
crispy_forms.bootstrap
import
AppendedText
...
...
@@ -47,8 +49,10 @@ class ArticleDetailView(DetailView):
return
context
class
ArticleEdit
(
UpdateView
):
class
ArticleEdit
(
PermissionRequiredMixin
,
UpdateView
):
model
=
Article
permission_required
=
'can_edit'
fields
=
[
'screenshot'
,
'url'
,
'lang'
,
'title'
,
'tags'
,
'extracts'
]
success_url
=
reverse_lazy
(
"rp:article-list"
)
...
...
apps/userprofile/admin.py
View file @
872a80d1
...
...
@@ -22,13 +22,19 @@ class UserProfileInline(admin.StackedInline):
class
UserProfileAdmin
(
UserAdmin
):
inlines
=
[
UserProfileInline
]
list_display
=
(
"username"
,
"email"
,
"first_name"
,
"last_name"
,
"is_staff"
,
"get_groups"
)
fieldsets
=
(
(
None
,
{
"fields"
:
(
"username"
,
"password"
)}),
(
_
(
"Personal info"
),
{
"fields"
:
(
"first_name"
,
"last_name"
,
"email"
)}),
(
_
(
"Permissions"
),
{
"fields"
:
(
"is_active"
,
"is_staff"
,
"is_superuser"
)}),
"fields"
:
(
"is_active"
,
"is_staff"
,
"is_superuser"
,
"groups"
)}),
)
def
get_groups
(
self
,
obj
):
return
", "
.
join
(
sorted
([
g
.
name
for
g
in
obj
.
groups
.
all
()]))
get_groups
.
short_description
=
_
(
"Groups"
)
admin
.
site
.
unregister
(
User
)
admin
.
site
.
register
(
User
,
UserProfileAdmin
)
project/settings/api.py
View file @
872a80d1
...
...
@@ -6,4 +6,8 @@ REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS"
:
(
"rest_framework.pagination."
"PageNumberPagination"
),
"PAGE_SIZE"
:
20
,
"DEFAULT_PERMISSION_CLASSES"
:
(
"rest_framework.permissions.IsAuthenticated"
,
)
}
project/settings/auth.py
View file @
872a80d1
...
...
@@ -4,7 +4,7 @@ User registration and login related settings
AUTH_USER_MODEL
=
"auth.User"
EXTENDED_USER_MODEL
=
"userprofile.Profile"
LOGIN_URL
=
"login"
LOGIN_URL
=
"
/accounts/
login"
AUTHENTICATION_BACKENDS
=
[
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment