Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
R
rp
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
14
Issues
14
List
Boards
Labels
Service Desk
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
La Quadrature du Net
rpteam
rp
Commits
872a80d1
Commit
872a80d1
authored
May 02, 2017
by
cynddl
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add initial authentication and authorization for views and API
parent
5ab9a7df
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
136 additions
and
74 deletions
+136
-74
apps/core/management/commands/init_groups.py
apps/core/management/commands/init_groups.py
+11
-4
apps/rp/api/mixins.py
apps/rp/api/mixins.py
+51
-0
apps/rp/api/views.py
apps/rp/api/views.py
+4
-53
apps/rp/migrations/0014_auto_20170501_1611.py
apps/rp/migrations/0014_auto_20170501_1611.py
+25
-0
apps/rp/models/article.py
apps/rp/models/article.py
+14
-7
apps/rp/templates/rp/article_list.html
apps/rp/templates/rp/article_list.html
+7
-2
apps/rp/urls.py
apps/rp/urls.py
+7
-5
apps/rp/views/articles.py
apps/rp/views/articles.py
+5
-1
apps/userprofile/admin.py
apps/userprofile/admin.py
+7
-1
project/settings/api.py
project/settings/api.py
+4
-0
project/settings/auth.py
project/settings/auth.py
+1
-1
No files found.
apps/core/management/commands/init_groups.py
View file @
872a80d1
...
@@ -3,13 +3,20 @@ from django.contrib.auth.models import Group, Permission
...
@@ -3,13 +3,20 @@ from django.contrib.auth.models import Group, Permission
groups
=
[
"jedi"
,
"padawan"
]
groups
=
[
"jedi"
,
"padawan"
]
permissions
=
{
permissions
=
{
"jedi"
:
[],
"jedi"
:
[
"padawans"
:
[]
"can_change_status"
,
"can_change_priority"
,
"can_vote"
,
"can_edit"
],
"padawan"
:
[
"can_vote"
,
"add_article"
]
}
}
class
Command
(
BaseCommand
):
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
):
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
import
viewsets
from
rest_framework.decorators
import
detail_route
from
rest_framework.response
import
Response
from
rp.models
import
Article
from
rp.models
import
Article
from
.serializers
import
ArticleSerializer
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
()
queryset
=
Article
.
objects
.
all
()
serializer_class
=
ArticleSerializer
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):
...
@@ -75,6 +75,7 @@ class Article(VoteMixin):
(
"can_change_status"
,
"Can change article status"
),
(
"can_change_status"
,
"Can change article status"
),
(
"can_change_priority"
,
"Can change article priority"
),
(
"can_change_priority"
,
"Can change article priority"
),
(
"can_vote"
,
"Can vote articles"
),
(
"can_vote"
,
"Can vote articles"
),
(
"can_edit"
,
"Can edit articles"
)
)
)
def
__str__
(
self
):
def
__str__
(
self
):
...
@@ -82,7 +83,8 @@ class Article(VoteMixin):
...
@@ -82,7 +83,8 @@ class Article(VoteMixin):
# Finite state logic
# Finite state logic
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'PUBLISHED'
)
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'PUBLISHED'
,
permission
=
"can_change_status"
)
def
publish
(
self
):
def
publish
(
self
):
self
.
published_at
=
datetime
.
now
()
self
.
published_at
=
datetime
.
now
()
...
@@ -98,14 +100,19 @@ class Article(VoteMixin):
...
@@ -98,14 +100,19 @@ class Article(VoteMixin):
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
permission
=
"can_change_priority"
)
permission
=
"can_change_priority"
)
def
set_priority
(
self
,
value
):
def
set_priority
(
self
):
self
.
priority
=
value
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
=
'DRAFT'
,
target
=
'DRAFT'
)
@
transition
(
field
=
status
,
source
=
'NEW'
,
@
transition
(
field
=
status
,
source
=
'NEW'
,
target
=
RETURN_VALUE
(
'NEW'
,
'DRAFT'
),
permission
=
"can_vote"
)
target
=
RETURN_VALUE
(
'NEW'
,
'DRAFT'
),
permission
=
"can_vote"
)
def
upvote
(
self
,
user_object
):
def
upvote
(
self
,
by
=
None
):
super
(
Article
,
self
).
upvote
(
user_object
)
super
(
Article
,
self
).
upvote
(
by
)
if
self
.
und_score
>=
ARTICLE_SCORE_THRESHOLD
:
if
self
.
und_score
>=
ARTICLE_SCORE_THRESHOLD
:
return
'DRAFT'
return
'DRAFT'
else
:
else
:
...
@@ -114,8 +121,8 @@ class Article(VoteMixin):
...
@@ -114,8 +121,8 @@ class Article(VoteMixin):
@
transition
(
field
=
status
,
source
=
'NEW'
,
target
=
'NEW'
,
permission
=
"can_vote"
)
@
transition
(
field
=
status
,
source
=
'NEW'
,
target
=
'NEW'
,
permission
=
"can_vote"
)
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
@
transition
(
field
=
status
,
source
=
'DRAFT'
,
target
=
'DRAFT'
,
permission
=
"can_vote"
)
permission
=
"can_vote"
)
def
downvote
(
self
,
user_object
):
def
downvote
(
self
,
by
=
None
):
super
(
Article
,
self
).
downvote
(
user_object
)
super
(
Article
,
self
).
downvote
(
by
)
# Content extraction
# Content extraction
...
...
apps/rp/templates/rp/article_list.html
View file @
872a80d1
...
@@ -218,8 +218,13 @@
...
@@ -218,8 +218,13 @@
}
}
function
call_priority
(
id
,
flag
)
{
function
call_priority
(
id
,
flag
)
{
var
url
=
"
/api/articles/
"
+
id
+
"
/priority/
"
+
(
flag
?
"
on/
"
:
"
off/
"
);
if
(
flag
)
{
$
.
post
(
url
,
{
'
priority
'
:
flag
},
function
response
(
data
)
{
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
"
);
$
(
"
#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
django.conf.urls
import
url
from
rp.views.articles
import
ArticleListFlux
,
ArticleEdit
,
ArticleDetailView
from
rp.views.articles
import
ArticleListFlux
,
ArticleEdit
,
ArticleDetailView
urlpatterns
=
[
urlpatterns
=
[
url
(
url
(
r
"^article/list/(?P<filter_view>\w+)"
,
r
"^article/list/(?P<filter_view>\w+)"
,
ArticleListFlux
.
as_view
(
),
login_required
(
ArticleListFlux
.
as_view
()
),
name
=
"article-list"
name
=
"article-list"
),
),
url
(
url
(
r
"^article/list"
,
r
"^article/list"
,
ArticleListFlux
.
as_view
(
),
login_required
(
ArticleListFlux
.
as_view
()
),
name
=
"article-list"
name
=
"article-list"
),
),
url
(
url
(
r
"^article/edit/(?P<pk>\d+)"
,
r
"^article/edit/(?P<pk>\d+)"
,
ArticleEdit
.
as_view
(
),
login_required
(
ArticleEdit
.
as_view
()
),
name
=
"article-edit"
name
=
"article-edit"
),
),
url
(
url
(
r
"^article/view/(?P<pk>\d+)"
,
r
"^article/view/(?P<pk>\d+)"
,
ArticleDetailView
.
as_view
(
),
login_required
(
ArticleDetailView
.
as_view
()
),
name
=
"article-view"
name
=
"article-view"
),
),
url
(
url
(
r
"^article/preview/(?P<pk>\d+)"
,
r
"^article/preview/(?P<pk>\d+)"
,
ArticleDetailView
.
as_view
(
preview
=
True
),
login_required
(
ArticleDetailView
.
as_view
(
preview
=
True
)
),
name
=
"article-preview"
name
=
"article-preview"
)
)
]
]
apps/rp/views/articles.py
View file @
872a80d1
...
@@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _
...
@@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _
from
django.urls
import
reverse
,
reverse_lazy
from
django.urls
import
reverse
,
reverse_lazy
from
django
import
forms
from
django
import
forms
from
django.contrib.auth.mixins
import
LoginRequiredMixin
,
PermissionRequiredMixin
from
crispy_forms.helper
import
FormHelper
from
crispy_forms.helper
import
FormHelper
from
crispy_forms.layout
import
Layout
,
Field
,
Div
,
HTML
from
crispy_forms.layout
import
Layout
,
Field
,
Div
,
HTML
from
crispy_forms.bootstrap
import
AppendedText
from
crispy_forms.bootstrap
import
AppendedText
...
@@ -47,8 +49,10 @@ class ArticleDetailView(DetailView):
...
@@ -47,8 +49,10 @@ class ArticleDetailView(DetailView):
return
context
return
context
class
ArticleEdit
(
UpdateView
):
class
ArticleEdit
(
PermissionRequiredMixin
,
UpdateView
):
model
=
Article
model
=
Article
permission_required
=
'can_edit'
fields
=
[
'screenshot'
,
'url'
,
'lang'
,
'title'
,
'tags'
,
'extracts'
]
fields
=
[
'screenshot'
,
'url'
,
'lang'
,
'title'
,
'tags'
,
'extracts'
]
success_url
=
reverse_lazy
(
"rp:article-list"
)
success_url
=
reverse_lazy
(
"rp:article-list"
)
...
...
apps/userprofile/admin.py
View file @
872a80d1
...
@@ -22,13 +22,19 @@ class UserProfileInline(admin.StackedInline):
...
@@ -22,13 +22,19 @@ class UserProfileInline(admin.StackedInline):
class
UserProfileAdmin
(
UserAdmin
):
class
UserProfileAdmin
(
UserAdmin
):
inlines
=
[
UserProfileInline
]
inlines
=
[
UserProfileInline
]
list_display
=
(
"username"
,
"email"
,
"first_name"
,
"last_name"
,
"is_staff"
,
"get_groups"
)
fieldsets
=
(
fieldsets
=
(
(
None
,
{
"fields"
:
(
"username"
,
"password"
)}),
(
None
,
{
"fields"
:
(
"username"
,
"password"
)}),
(
_
(
"Personal info"
),
{
"fields"
:
(
"first_name"
,
"last_name"
,
"email"
)}),
(
_
(
"Personal info"
),
{
"fields"
:
(
"first_name"
,
"last_name"
,
"email"
)}),
(
_
(
"Permissions"
),
{
(
_
(
"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
.
unregister
(
User
)
admin
.
site
.
register
(
User
,
UserProfileAdmin
)
admin
.
site
.
register
(
User
,
UserProfileAdmin
)
project/settings/api.py
View file @
872a80d1
...
@@ -6,4 +6,8 @@ REST_FRAMEWORK = {
...
@@ -6,4 +6,8 @@ REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS"
:
(
"rest_framework.pagination."
"DEFAULT_PAGINATION_CLASS"
:
(
"rest_framework.pagination."
"PageNumberPagination"
),
"PageNumberPagination"
),
"PAGE_SIZE"
:
20
,
"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
...
@@ -4,7 +4,7 @@ User registration and login related settings
AUTH_USER_MODEL
=
"auth.User"
AUTH_USER_MODEL
=
"auth.User"
EXTENDED_USER_MODEL
=
"userprofile.Profile"
EXTENDED_USER_MODEL
=
"userprofile.Profile"
LOGIN_URL
=
"login"
LOGIN_URL
=
"
/accounts/
login"
AUTHENTICATION_BACKENDS
=
[
AUTHENTICATION_BACKENDS
=
[
...
...
Write
Preview
Markdown
is supported
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