mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-05 23:18:53 -05:00
feat: approve users, edit users through manage users page (#1383)
This commit is contained in:
parent
8e8454d8c2
commit
cbef629baf
10
cms/auth_backends.py
Normal file
10
cms/auth_backends.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class ApprovalBackend(ModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
can_authenticate = super().user_can_authenticate(user)
|
||||
if can_authenticate and settings.USERS_NEEDS_TO_BE_APPROVED and not user.is_superuser:
|
||||
return getattr(user, 'is_approved', False)
|
||||
return can_authenticate
|
||||
23
cms/middleware.py
Normal file
23
cms/middleware.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ApprovalMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED and request.user.is_authenticated and not request.user.is_superuser and not getattr(request.user, 'is_approved', False):
|
||||
allowed_paths = [
|
||||
reverse('approval_required'),
|
||||
reverse('account_logout'),
|
||||
]
|
||||
if request.path not in allowed_paths:
|
||||
if request.path.startswith('/api/'):
|
||||
return JsonResponse({'detail': 'User account not approved.'}, status=403)
|
||||
return redirect('approval_required')
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@ -128,6 +128,10 @@ USERS_CAN_SELF_REGISTER = True
|
||||
|
||||
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = ["xxx.com", "emaildomainwhatever.com"]
|
||||
|
||||
# by default users do not need to be approved. If this is set to True, then new users
|
||||
# will have to be approved before they can login successfully
|
||||
USERS_NEEDS_TO_BE_APPROVED = False
|
||||
|
||||
# Comma separated list of domains: ["organization.com", "private.organization.com", "org2.com"]
|
||||
# Empty list disables.
|
||||
ALLOWED_DOMAINS_FOR_USER_REGISTRATION = []
|
||||
@ -501,6 +505,10 @@ ALLOW_CUSTOM_MEDIA_URLS = False
|
||||
# Whether to allow anonymous users to list all users
|
||||
ALLOW_ANONYMOUS_USER_LISTING = True
|
||||
|
||||
# Who can see the members page
|
||||
# valid choices are all, editors, admins
|
||||
CAN_SEE_MEMBERS_PAGE = "all"
|
||||
|
||||
# Maximum number of media a user can upload
|
||||
NUMBER_OF_MEDIA_USER_CAN_UPLOAD = 100
|
||||
|
||||
@ -517,6 +525,9 @@ USER_CAN_TRANSCRIBE_VIDEO = True
|
||||
# Whisper transcribe options - https://github.com/openai/whisper
|
||||
WHISPER_MODEL = "base"
|
||||
|
||||
# show a custom text in the sidebar footer, otherwise the default will be shown if this is empty
|
||||
SIDEBAR_FOOTER_TEXT = ""
|
||||
|
||||
try:
|
||||
# keep a local_settings.py file for local overrides
|
||||
from .local_settings import * # noqa
|
||||
@ -558,3 +569,12 @@ except ImportError:
|
||||
if GLOBAL_LOGIN_REQUIRED:
|
||||
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE.insert(auth_index + 1, "django.contrib.auth.middleware.LoginRequiredMiddleware")
|
||||
|
||||
|
||||
if USERS_NEEDS_TO_BE_APPROVED:
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'cms.auth_backends.ApprovalBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
)
|
||||
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE.insert(auth_index + 1, "cms.middleware.ApprovalMiddleware")
|
||||
|
||||
@ -1 +1 @@
|
||||
VERSION = "6.5.2"
|
||||
VERSION = "6.6.0"
|
||||
|
||||
@ -519,6 +519,20 @@ ALLOW_ANONYMOUS_USER_LISTING = False
|
||||
When set to False, only logged-in users will be able to access the user listing API endpoint.
|
||||
|
||||
|
||||
### 5.27 Control who can see the members page
|
||||
|
||||
By default `CAN_SEE_MEMBERS_PAGE = "all"` means that all registered users can see the members page. Other valid options are:
|
||||
|
||||
- **editors**, only MediaCMS editors can view the page
|
||||
- **admins**, only MediaCMS admins can view the page
|
||||
|
||||
|
||||
### 5.28 Require user approval on registration
|
||||
|
||||
By default, users do not require approval, so they can login immediately after registration (if registration is open). However, if the parameter `USERS_NEEDS_TO_BE_APPROVED` is set to `True`, they will first have to have their accounts approved by an administrator before they can successfully sign in.
|
||||
Administrators can approve users through the following ways: 1. through Django administration, 2. through the users management page, 3. through editing the profile page directly. In all cases, set 'Is approved' to True.
|
||||
|
||||
|
||||
## 6. Manage pages
|
||||
to be written
|
||||
|
||||
|
||||
@ -26,10 +26,22 @@ def stuff(request):
|
||||
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
|
||||
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
|
||||
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE
|
||||
ret["SIDEBAR_FOOTER_TEXT"] = settings.SIDEBAR_FOOTER_TEXT
|
||||
ret["POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY"] = settings.POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY
|
||||
ret["IS_MEDIACMS_ADMIN"] = request.user.is_superuser
|
||||
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
|
||||
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(request.user)
|
||||
ret["USERS_NEEDS_TO_BE_APPROVED"] = settings.USERS_NEEDS_TO_BE_APPROVED
|
||||
|
||||
can_see_members_page = False
|
||||
if request.user.is_authenticated:
|
||||
if settings.CAN_SEE_MEMBERS_PAGE == "all":
|
||||
can_see_members_page = True
|
||||
elif settings.CAN_SEE_MEMBERS_PAGE == "editors" and is_mediacms_editor(request.user):
|
||||
can_see_members_page = True
|
||||
elif settings.CAN_SEE_MEMBERS_PAGE == "admins" and request.user.is_superuser:
|
||||
can_see_members_page = True
|
||||
ret["CAN_SEE_MEMBERS_PAGE"] = can_see_members_page
|
||||
ret["ALLOW_RATINGS"] = settings.ALLOW_RATINGS
|
||||
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
|
||||
ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from drf_yasg import openapi as openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import status
|
||||
@ -219,6 +221,13 @@ class UserList(APIView):
|
||||
elif role == "editor":
|
||||
qs = qs.filter(is_editor=True)
|
||||
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED:
|
||||
is_approved = request.GET.get("is_approved")
|
||||
if is_approved == "true":
|
||||
qs = qs.filter(is_approved=True)
|
||||
elif is_approved == "false":
|
||||
qs = qs.filter(Q(is_approved=False) | Q(is_approved__isnull=True))
|
||||
|
||||
users = qs.order_by(f"{ordering}{sort_by}")
|
||||
|
||||
paginator = pagination_class()
|
||||
|
||||
@ -110,6 +110,9 @@ urlpatterns = [
|
||||
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED:
|
||||
urlpatterns.append(re_path(r"^approval_required", views.approval_required, name="approval_required"))
|
||||
|
||||
if hasattr(settings, "USE_SAML") and settings.USE_SAML:
|
||||
urlpatterns.append(re_path(r"^saml/metadata", views.saml_metadata, name="saml-metadata"))
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
# Import all views for backward compatibility
|
||||
|
||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
||||
from .categories import CategoryList, TagList # noqa: F401
|
||||
from .comments import CommentDetail, CommentList # noqa: F401
|
||||
@ -10,6 +11,7 @@ from .media import MediaList # noqa: F401
|
||||
from .media import MediaSearch # noqa: F401
|
||||
from .pages import about # noqa: F401
|
||||
from .pages import add_subtitle # noqa: F401
|
||||
from .pages import approval_required # noqa: F401
|
||||
from .pages import categories # noqa: F401
|
||||
from .pages import contact # noqa: F401
|
||||
from .pages import edit_chapters # noqa: F401
|
||||
|
||||
@ -54,6 +54,11 @@ def about(request):
|
||||
return render(request, "cms/about.html", context)
|
||||
|
||||
|
||||
def approval_required(request):
|
||||
"""User needs approval view"""
|
||||
return render(request, "cms/user_needs_approval.html", {})
|
||||
|
||||
|
||||
def setlanguage(request):
|
||||
"""Set Language view"""
|
||||
|
||||
@ -517,6 +522,12 @@ def manage_comments(request):
|
||||
def members(request):
|
||||
"""List members view"""
|
||||
|
||||
if settings.CAN_SEE_MEMBERS_PAGE == "editors" and not is_mediacms_editor(request.user):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
if settings.CAN_SEE_MEMBERS_PAGE == "admins" and not request.user.is_superuser:
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
context = {}
|
||||
return render(request, "cms/members.html", context)
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { usePopup } from '../../../utils/hooks/';
|
||||
import { usePopup, useUser } from '../../../utils/hooks/';
|
||||
import { PageStore } from '../../../utils/stores/';
|
||||
import { csrfToken } from '../../../utils/helpers/';
|
||||
import { PopupMain } from '../../_shared';
|
||||
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon.jsx';
|
||||
import { ManageItemDate } from './ManageMediaItem';
|
||||
@ -38,34 +39,86 @@ function ManageItemUsername(props) {
|
||||
return <i className="non-available">N/A</i>;
|
||||
}
|
||||
|
||||
function ManageItemCommentActions(props) {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||
const [isOpenPopup, setIsOpenPopup] = useState(false);
|
||||
function ManageUsersItemActions(props) {
|
||||
const { userCan } = useUser();
|
||||
const [deletePopupRef, DeletePopupContent, DeletePopupTrigger] = usePopup();
|
||||
const [passwordPopupRef, PasswordPopupContent, PasswordPopupTrigger] = usePopup();
|
||||
const [approvePopupRef, ApprovePopupContent, ApprovePopupTrigger] = usePopup();
|
||||
|
||||
function onPopupShow() {
|
||||
setIsOpenPopup(true);
|
||||
}
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
|
||||
function onPopupHide() {
|
||||
setIsOpenPopup(false);
|
||||
}
|
||||
const [isDeleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isPasswordOpen, setPasswordOpen] = useState(false);
|
||||
const [isApproveOpen, setApproveOpen] = useState(false);
|
||||
|
||||
function onCancel() {
|
||||
popupContentRef.current.tryToHide();
|
||||
if ('function' === typeof props.onCancel) {
|
||||
props.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function onProceed() {
|
||||
popupContentRef.current.tryToHide();
|
||||
function onProceedDelete() {
|
||||
deletePopupRef.current.tryToHide();
|
||||
if ('function' === typeof props.onProceed) {
|
||||
props.onProceed();
|
||||
}
|
||||
}
|
||||
function onCancelDelete() {
|
||||
deletePopupRef.current.tryToHide();
|
||||
}
|
||||
|
||||
function handlePasswordChangeSubmit(e) {
|
||||
e.preventDefault();
|
||||
props.setMessage({ type: '', text: '' });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'change_password');
|
||||
formData.append('password', newPassword);
|
||||
|
||||
fetch(`/api/v1/users/${props.username}`, {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
headers: { 'X-CSRFToken': csrfToken() },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
return res.json().then((data) => {
|
||||
throw new Error(data.detail || 'Failed to change password.');
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
sessionStorage.setItem('user-management-message', JSON.stringify({ type: 'success', text: 'Password changed successfully.' }));
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((err) => {
|
||||
props.setMessage({ type: 'error', text: err.message });
|
||||
});
|
||||
}
|
||||
|
||||
function handleApproveUser() {
|
||||
props.setMessage({ type: '', text: '' });
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'approve_user');
|
||||
|
||||
fetch(`/api/v1/users/${props.username}`, {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
headers: { 'X-CSRFToken': csrfToken() },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
return res.json().then((data) => {
|
||||
throw new Error(data.detail || 'Failed to approve user.');
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
sessionStorage.setItem('user-management-message', JSON.stringify({ type: 'success', text: 'User approved successfully.' }));
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((err) => {
|
||||
props.setMessage({ type: 'error', text: err.message });
|
||||
});
|
||||
}
|
||||
|
||||
const positionState = { updating: false, pending: 0 };
|
||||
|
||||
const onWindowResize = useCallback(function () {
|
||||
if (positionState.updating) {
|
||||
positionState.pending = positionState.pending + 1;
|
||||
@ -98,6 +151,8 @@ function ManageItemCommentActions(props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isOpenPopup = isDeleteOpen || isPasswordOpen || isApproveOpen;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenPopup) {
|
||||
PageStore.on('window_scroll', onWindowResize);
|
||||
@ -111,11 +166,94 @@ function ManageItemCommentActions(props) {
|
||||
|
||||
return (
|
||||
<div ref={props.containerRef} className="actions">
|
||||
<PopupTrigger contentRef={popupContentRef}>
|
||||
<PasswordPopupTrigger contentRef={passwordPopupRef}>
|
||||
<button>Change password</button>
|
||||
</PasswordPopupTrigger>
|
||||
{userCan.usersNeedsToBeApproved && !props.is_approved && (
|
||||
<>
|
||||
<span className="seperator">|</span>
|
||||
<ApprovePopupTrigger contentRef={approvePopupRef}>
|
||||
<button>Approve</button>
|
||||
</ApprovePopupTrigger>
|
||||
</>
|
||||
)}
|
||||
<span className="seperator">|</span>
|
||||
<DeletePopupTrigger contentRef={deletePopupRef}>
|
||||
<button title={'Delete "' + props.name + '"'}>Delete</button>
|
||||
</PopupTrigger>
|
||||
</DeletePopupTrigger>
|
||||
|
||||
<PopupContent contentRef={popupContentRef} showCallback={onPopupShow} hideCallback={onPopupHide}>
|
||||
<PasswordPopupContent
|
||||
contentRef={passwordPopupRef}
|
||||
showCallback={() => setPasswordOpen(true)}
|
||||
hideCallback={() => {
|
||||
setPasswordOpen(false);
|
||||
props.setMessage({ type: '', text: '' });
|
||||
}}
|
||||
>
|
||||
<PopupMain>
|
||||
<form onSubmit={handlePasswordChangeSubmit}>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Change Password for {props.name}</span>
|
||||
<span className="popup-message-main">
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="New Password"
|
||||
required
|
||||
style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="button-link cancel-profile-removal"
|
||||
onClick={() => passwordPopupRef.current.tryToHide()}
|
||||
>
|
||||
CANCEL
|
||||
</button>
|
||||
<button type="submit" className="button-link proceed-profile-removal">
|
||||
SUBMIT
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
</PopupMain>
|
||||
</PasswordPopupContent>
|
||||
|
||||
<ApprovePopupContent
|
||||
contentRef={approvePopupRef}
|
||||
showCallback={() => setApproveOpen(true)}
|
||||
hideCallback={() => {
|
||||
setApproveOpen(false);
|
||||
props.setMessage({ type: '', text: '' });
|
||||
}}
|
||||
>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Approve User</span>
|
||||
<span className="popup-message-main">
|
||||
{'Are you sure you want to approve "' + props.name + '"?'}
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-profile-removal" onClick={() => approvePopupRef.current.tryToHide()}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-profile-removal" onClick={handleApproveUser}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</ApprovePopupContent>
|
||||
|
||||
<DeletePopupContent
|
||||
contentRef={deletePopupRef}
|
||||
showCallback={() => setDeleteOpen(true)}
|
||||
hideCallback={() => setDeleteOpen(false)}
|
||||
>
|
||||
<PopupMain>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Member removal</span>
|
||||
@ -123,15 +261,15 @@ function ManageItemCommentActions(props) {
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button className="button-link cancel-profile-removal" onClick={onCancel}>
|
||||
<button className="button-link cancel-profile-removal" onClick={onCancelDelete}>
|
||||
CANCEL
|
||||
</button>
|
||||
<button className="button-link proceed-profile-removal" onClick={onProceed}>
|
||||
<button className="button-link proceed-profile-removal" onClick={onProceedDelete}>
|
||||
PROCEED
|
||||
</button>
|
||||
</span>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</DeletePopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -168,10 +306,14 @@ export function ManageUsersItem(props) {
|
||||
</div>
|
||||
<div className="mi-name">
|
||||
<ManageItemName name={props.name} url={props.url} />
|
||||
<ManageItemCommentActions
|
||||
<ManageUsersItemActions
|
||||
containerRef={actionsContainerRef}
|
||||
name={props.name || props.username}
|
||||
username={props.username}
|
||||
is_approved={props.is_approved}
|
||||
onProceed={onClickProceed}
|
||||
onUserUpdate={props.onUserUpdate}
|
||||
setMessage={props.setMessage}
|
||||
/>
|
||||
</div>
|
||||
<div className="mi-username">
|
||||
@ -213,6 +355,17 @@ export function ManageUsersItem(props) {
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{props.has_approved ? (
|
||||
<div className="mi-approved">
|
||||
{void 0 === props.is_approved || props.is_approved === null ? (
|
||||
<i className="non-available">N/A</i>
|
||||
) : props.is_approved ? (
|
||||
<MaterialIcon type="check_circle" />
|
||||
) : (
|
||||
<MaterialIcon type="cancel" />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mi-featured">
|
||||
{void 0 === props.is_featured ? (
|
||||
<i className="non-available">N/A</i>
|
||||
@ -234,18 +387,25 @@ ManageUsersItem.propTypes = {
|
||||
add_date: PropTypes.string,
|
||||
is_featured: PropTypes.bool,
|
||||
onCheckRow: PropTypes.func,
|
||||
onUserUpdate: PropTypes.func,
|
||||
setMessage: PropTypes.func,
|
||||
selectedRow: PropTypes.bool.isRequired,
|
||||
hideDeleteAction: PropTypes.bool.isRequired,
|
||||
has_roles: PropTypes.bool,
|
||||
has_verified: PropTypes.bool,
|
||||
has_trusted: PropTypes.bool,
|
||||
has_approved: PropTypes.bool,
|
||||
roles: PropTypes.array,
|
||||
is_verified: PropTypes.bool,
|
||||
is_trusted: PropTypes.bool,
|
||||
is_approved: PropTypes.bool,
|
||||
};
|
||||
|
||||
ManageUsersItem.defaultProps = {
|
||||
has_roles: false,
|
||||
has_verified: false,
|
||||
has_trusted: false,
|
||||
has_approved: false,
|
||||
onUserUpdate: () => {},
|
||||
setMessage: () => {},
|
||||
};
|
||||
|
||||
@ -45,6 +45,7 @@ export function ManageUsersItemHeader(props) {
|
||||
{props.has_roles ? <div className="mi-role">Role</div> : null}
|
||||
{props.has_verified ? <div className="mi-verified">Verified</div> : null}
|
||||
{props.has_trusted ? <div className="mi-trusted">Trusted</div> : null}
|
||||
{props.has_approved ? <div className="mi-approved">Approved</div> : null}
|
||||
<div className="mi-featured">Featured</div>
|
||||
</div>
|
||||
);
|
||||
@ -59,10 +60,12 @@ ManageUsersItemHeader.propTypes = {
|
||||
has_roles: PropTypes.bool,
|
||||
has_verified: PropTypes.bool,
|
||||
has_trusted: PropTypes.bool,
|
||||
has_approved: PropTypes.bool,
|
||||
};
|
||||
|
||||
ManageUsersItemHeader.defaultProps = {
|
||||
has_roles: false,
|
||||
has_verified: false,
|
||||
has_trusted: false,
|
||||
has_approved: false,
|
||||
};
|
||||
|
||||
@ -12,6 +12,89 @@ import { translateString } from '../../../utils/helpers/';
|
||||
|
||||
import './ManageItemList.scss';
|
||||
|
||||
function AddNewUser({ onUserAdded, setMessage }) {
|
||||
const [popupRef, PopupContent, PopupTrigger] = usePopup();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
|
||||
function clearForm() {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setEmail('');
|
||||
setName('');
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
if (setMessage) {
|
||||
setMessage({ type: '', text: '' });
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
formData.append('email', email);
|
||||
formData.append('name', name);
|
||||
|
||||
fetch('/api/v1/users', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-CSRFToken': csrfToken() },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
return res.json().then((data) => {
|
||||
throw new Error(data.detail || 'Failed to create user.');
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
sessionStorage.setItem('user-management-message', JSON.stringify({ type: 'success', text: 'User created successfully.' }));
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (setMessage) {
|
||||
setMessage({ type: 'error', text: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="add-new-user-container">
|
||||
<PopupTrigger contentRef={popupRef}>
|
||||
<button className="add-new-user-btn">Add New User</button>
|
||||
</PopupTrigger>
|
||||
<PopupContent contentRef={popupRef} hideCallback={clearForm}>
|
||||
<PopupMain>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="popup-message">
|
||||
<span className="popup-message-title">Add New User</span>
|
||||
<div className="popup-message-main">
|
||||
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" required style={{ width: '100%', padding: '8px', boxSizing: 'border-box', marginBottom: '10px' }} />
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" required style={{ width: '100%', padding: '8px', boxSizing: 'border-box', marginBottom: '10px' }} />
|
||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required style={{ width: '100%', padding: '8px', boxSizing: 'border-box', marginBottom: '10px' }} />
|
||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" required style={{ width: '100%', padding: '8px', boxSizing: 'border-box', marginBottom: '10px' }} />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="popup-message-bottom">
|
||||
<button type="button" className="button-link cancel-profile-removal" onClick={() => popupRef.current.tryToHide()}>CANCEL</button>
|
||||
<button type="submit" className="button-link proceed-profile-removal">SUBMIT</button>
|
||||
</span>
|
||||
</form>
|
||||
</PopupMain>
|
||||
</PopupContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
AddNewUser.propTypes = {
|
||||
onUserAdded: PropTypes.func,
|
||||
setMessage: PropTypes.func,
|
||||
};
|
||||
|
||||
function useManageItemList(props, itemsListRef) {
|
||||
let previousItemsLength = 0;
|
||||
|
||||
@ -440,6 +523,35 @@ export function ManageItemList(props) {
|
||||
onItemsLoad,
|
||||
] = useManageItemListSync(props);
|
||||
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
useEffect(() => {
|
||||
const storedMessage = sessionStorage.getItem('user-management-message');
|
||||
if (storedMessage) {
|
||||
setMessage(JSON.parse(storedMessage));
|
||||
sessionStorage.removeItem('user-management-message');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (message.text) {
|
||||
const timer = setTimeout(() => setMessage({ type: '', text: '' }), 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
function refreshList() {
|
||||
if (props.onPageChange && parsedRequestUrl) {
|
||||
const queryParams = new URLSearchParams(parsedRequestUrlQuery || '');
|
||||
const currentPage = queryParams.get('page') || '1';
|
||||
const clickedPageUrl = pageUrl(parsedRequestUrl, pageUrlQuery(parsedRequestUrlQuery, currentPage));
|
||||
props.onPageChange(clickedPageUrl, currentPage);
|
||||
} else {
|
||||
// Fallback for when onPageChange is not available
|
||||
setListHandler(new ManageItemsListHandler(props.pageItems, props.maxItems, props.requestUrl, onItemsCount, onItemsLoad));
|
||||
}
|
||||
}
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [selectedAllItems, setSelectedAllItems] = useState(false);
|
||||
|
||||
@ -592,16 +704,22 @@ export function ManageItemList(props) {
|
||||
|
||||
return () => {
|
||||
if (listHandler) {
|
||||
listHandler.cancelAll();
|
||||
// listHandler.cancelAll();
|
||||
setListHandler(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [props.requestUrl]);
|
||||
|
||||
return !countedItems ? (
|
||||
<PendingItemsList className={classname.listOuter} />
|
||||
) : !items.length ? null : (
|
||||
) : (
|
||||
<div className={classname.listOuter}>
|
||||
{message.text && (
|
||||
<div className={`message ${message.type === 'error' ? 'error' : 'success'}`}>{message.text}</div>
|
||||
)}
|
||||
{'users' === props.manageType && <AddNewUser onUserAdded={refreshList} setMessage={setMessage} />}
|
||||
{!items.length ? null : (
|
||||
<>
|
||||
<ManageItemsOptions
|
||||
totalItems={totalItems}
|
||||
pageItems={props.pageItems}
|
||||
@ -622,6 +740,8 @@ export function ManageItemList(props) {
|
||||
selectedItems: selectedItems,
|
||||
selectedAllItems: selectedAllItems,
|
||||
onDelete: deleteItem,
|
||||
onUserUpdate: refreshList,
|
||||
setMessage: setMessage,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@ -636,6 +756,8 @@ export function ManageItemList(props) {
|
||||
pagesSize={listHandler.totalPages()}
|
||||
onProceedRemoval={onBulkItemsRemoval}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -146,8 +146,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
> div {
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
min-width: 98px;
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
vertical-align: middle;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
|
||||
.dark_theme & {
|
||||
@ -160,7 +165,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> * {
|
||||
> div {
|
||||
border-color: #eaeaea;
|
||||
|
||||
.dark_theme & {
|
||||
@ -188,17 +193,14 @@
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
min-width: 98px;
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mi-title,
|
||||
.mi-name {
|
||||
.actions {
|
||||
.seperator {
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
@ -252,7 +254,7 @@
|
||||
}
|
||||
|
||||
&.manage-media-item {
|
||||
> * {
|
||||
> div {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
@ -306,8 +308,8 @@
|
||||
}
|
||||
|
||||
&.manage-users-item {
|
||||
> * {
|
||||
width: math.div(1,8) * 100%;
|
||||
> div {
|
||||
width: math.div(1, 9) * 100%;
|
||||
}
|
||||
|
||||
.mi-added,
|
||||
@ -315,20 +317,23 @@
|
||||
.mi-featured,
|
||||
.mi-verified,
|
||||
.mi-trusted,
|
||||
.mi-approved,
|
||||
.mi-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mi-name,
|
||||
.mi-username {
|
||||
min-width: 240px;
|
||||
min-width: 200px;
|
||||
width: 50%;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mi-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mi-checkbox {
|
||||
min-width: 48px;
|
||||
width: 48px;
|
||||
@ -344,7 +349,7 @@
|
||||
}
|
||||
|
||||
&.manage-comments-item {
|
||||
> * {
|
||||
> div {
|
||||
width: 16%;
|
||||
}
|
||||
|
||||
@ -457,7 +462,7 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
> * {
|
||||
> div {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-right: 0;
|
||||
@ -469,7 +474,6 @@
|
||||
position: relative;
|
||||
display: inline;
|
||||
vertical-align: top;
|
||||
background-color: yellow;
|
||||
|
||||
.material-icons {
|
||||
width: auto;
|
||||
@ -479,7 +483,7 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> * {
|
||||
> span {
|
||||
opacity: 0.25;
|
||||
|
||||
position: absolute;
|
||||
@ -501,7 +505,7 @@
|
||||
text-decoration: underline;
|
||||
|
||||
.mi-col-sort-icons {
|
||||
> * {
|
||||
> span {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
@ -509,7 +513,7 @@
|
||||
|
||||
&.desc {
|
||||
.mi-col-sort-icons {
|
||||
> *:last-child {
|
||||
> span:last-child {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
@ -517,7 +521,7 @@
|
||||
|
||||
&.asc {
|
||||
.mi-col-sort-icons {
|
||||
> *:first-child {
|
||||
> span:first-child {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
@ -533,6 +537,66 @@
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
&.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
&.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-user-container {
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
float: right;
|
||||
|
||||
.popup-message-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
color: var(--popup-msg-main-text-color);
|
||||
|
||||
&.proceed-profile-removal {
|
||||
color: var(--default-theme-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-user-btn {
|
||||
padding: 0 16px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
color: var(--default-theme-color);
|
||||
border: 1px solid var(--default-theme-color);
|
||||
background: var(--user-action-form-inner-bg-color);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 1px 4px 0 rgba(17, 17, 17, 0.06);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: var(--default-theme-color);
|
||||
color: var(--user-action-form-inner-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.manage-items-bulk-action {
|
||||
position: relative;
|
||||
width: auto;
|
||||
|
||||
@ -5,6 +5,7 @@ import { ManageCommentsItem } from '../../ManageItem/ManageCommentsItem';
|
||||
import { ManageMediaItemHeader } from '../../ManageItem/ManageMediaItemHeader';
|
||||
import { ManageUsersItemHeader } from '../../ManageItem/ManageUsersItemHeader';
|
||||
import { ManageCommentsItemHeader } from '../../ManageItem/ManageCommentsItemHeader';
|
||||
import { useUser } from '../../../../utils/hooks/';
|
||||
|
||||
function useManageItem(props) {
|
||||
const itemData = props.item;
|
||||
@ -15,6 +16,8 @@ function useManageItem(props) {
|
||||
selectedRow: props.selectedRow,
|
||||
onProceedRemoval: props.onProceedRemoval,
|
||||
hideDeleteAction: props.hideDeleteAction,
|
||||
onUserUpdate: props.onUserUpdate,
|
||||
setMessage: props.setMessage,
|
||||
};
|
||||
|
||||
return [itemData, itemProps];
|
||||
@ -44,6 +47,7 @@ function ListManageMediaItem(props) {
|
||||
}
|
||||
|
||||
function ListManageUserItem(props) {
|
||||
const { userCan } = useUser();
|
||||
const [itemData, itemProps] = useManageItem(props);
|
||||
|
||||
const roles = [];
|
||||
@ -70,6 +74,8 @@ function ListManageUserItem(props) {
|
||||
has_roles: void 0 !== itemData.is_editor || void 0 !== itemData.is_manager,
|
||||
has_verified: void 0 !== itemData.email_is_verified,
|
||||
has_trusted: void 0 !== itemData.advancedUser,
|
||||
is_approved: itemData.is_approved,
|
||||
has_approved: userCan.usersNeedsToBeApproved && void 0 !== itemData.is_approved,
|
||||
};
|
||||
|
||||
return <ManageUsersItem {...args} />;
|
||||
@ -99,6 +105,8 @@ function ListManageItem(props) {
|
||||
hideDeleteAction: false,
|
||||
onCheckRow: props.onCheckRow,
|
||||
onProceedRemoval: props.onProceedRemoval,
|
||||
onUserUpdate: props.onUserUpdate,
|
||||
setMessage: props.setMessage,
|
||||
};
|
||||
|
||||
if ('media' === props.type) {
|
||||
@ -117,6 +125,7 @@ function ListManageItem(props) {
|
||||
}
|
||||
|
||||
function ListManageItemHeader(props) {
|
||||
const { userCan } = useUser();
|
||||
const args = {
|
||||
sort: props.sort,
|
||||
order: props.order,
|
||||
@ -134,6 +143,10 @@ function ListManageItemHeader(props) {
|
||||
props.items.length && (void 0 !== props.items[0].is_editor || void 0 !== props.items[0].is_manager);
|
||||
args.has_verified = props.items.length && void 0 !== props.items[0].email_is_verified;
|
||||
args.has_trusted = props.items.length && void 0 !== props.items[0].advancedUser;
|
||||
args.has_approved =
|
||||
userCan.usersNeedsToBeApproved &&
|
||||
props.items.length &&
|
||||
void 0 !== props.items[0].is_approved;
|
||||
return <ManageUsersItemHeader {...args} />;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PageStore } from '../../utils/stores/';
|
||||
import { useUser } from '../../utils/hooks/';
|
||||
import { FilterOptions } from '../_shared';
|
||||
|
||||
import './ManageItemList-filters.scss';
|
||||
@ -11,12 +12,19 @@ const filters = {
|
||||
{ id: 'editor', title: 'Editor' },
|
||||
{ id: 'manager', title: 'Manager' },
|
||||
],
|
||||
approved: [
|
||||
{ id: 'all', title: 'All' },
|
||||
{ id: 'true', title: 'Yes' },
|
||||
{ id: 'false', title: 'No' },
|
||||
],
|
||||
};
|
||||
|
||||
export function ManageUsersFilters(props) {
|
||||
const { userCan } = useUser();
|
||||
const [isHidden, setIsHidden] = useState(props.hidden);
|
||||
|
||||
const [role, setFilterRole] = useState('all');
|
||||
const [approved, setFilterApproved] = useState('all');
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const innerContainerRef = useRef(null);
|
||||
@ -30,6 +38,7 @@ export function ManageUsersFilters(props) {
|
||||
function onFilterSelect(ev) {
|
||||
const args = {
|
||||
role: role,
|
||||
is_approved: approved,
|
||||
};
|
||||
|
||||
switch (ev.currentTarget.getAttribute('filter')) {
|
||||
@ -38,6 +47,11 @@ export function ManageUsersFilters(props) {
|
||||
props.onFiltersUpdate(args);
|
||||
setFilterRole(args.role);
|
||||
break;
|
||||
case 'approved':
|
||||
args.is_approved = ev.currentTarget.getAttribute('value');
|
||||
props.onFiltersUpdate(args);
|
||||
setFilterApproved(args.is_approved);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,6 +74,14 @@ export function ManageUsersFilters(props) {
|
||||
<FilterOptions id={'role'} options={filters.role} selected={role} onSelect={onFilterSelect} />
|
||||
</div>
|
||||
</div>
|
||||
{userCan.usersNeedsToBeApproved ? (
|
||||
<div className="mi-filter">
|
||||
<div className="mi-filter-title">APPROVED</div>
|
||||
<div className="mi-filter-options">
|
||||
<FilterOptions id={'approved'} options={filters.approved} selected={approved} onSelect={onFilterSelect} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -102,7 +102,11 @@ export function SidebarNavigationMenu() {
|
||||
});
|
||||
}
|
||||
|
||||
if (PageStore.get('config-enabled').pages.members && PageStore.get('config-enabled').pages.members.enabled) {
|
||||
if (
|
||||
PageStore.get('config-enabled').pages.members &&
|
||||
PageStore.get('config-enabled').pages.members.enabled &&
|
||||
userCan.canSeeMembersPage
|
||||
) {
|
||||
items.push({
|
||||
link: links.members,
|
||||
icon: 'people',
|
||||
|
||||
@ -14,6 +14,8 @@ export function init(user, features) {
|
||||
register: true,
|
||||
addMedia: false,
|
||||
editProfile: false,
|
||||
canSeeMembersPage: true,
|
||||
usersNeedsToBeApproved: true,
|
||||
changePassword: true,
|
||||
deleteProfile: false,
|
||||
readComment: true,
|
||||
@ -91,6 +93,8 @@ export function init(user, features) {
|
||||
}
|
||||
}
|
||||
|
||||
MEMBER.can.canSeeMembersPage = true === user.can.canSeeMembersPage;
|
||||
MEMBER.can.usersNeedsToBeApproved = true === user.can.usersNeedsToBeApproved;
|
||||
MEMBER.can.addMedia = true === user.can.addMedia;
|
||||
MEMBER.can.editProfile = true === user.can.editProfile;
|
||||
MEMBER.can.readComment = false === user.can.readComment ? false : true;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
templates/cms/user_needs_approval.html
Normal file
13
templates/cms/user_needs_approval.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block headtitle %} | Account needs approval{% endblock headtitle %}
|
||||
|
||||
{% block innercontent %}
|
||||
<div class="user-action-form-wrap">
|
||||
<div class="user-action-form-inner">
|
||||
<h1>Account Pending Approval</h1>
|
||||
<p>Your account is currently pending approval from an administrator.</p>
|
||||
<p><a href="{% url 'account_logout' %}">Logout</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock innercontent %}
|
||||
@ -16,9 +16,11 @@ MediaCMS.user = {
|
||||
deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %},
|
||||
editProfile: {% if CAN_EDIT %}true{% else %}false{% endif %},
|
||||
deleteProfile: {% if CAN_DELETE %}true{% else %}false{% endif %},
|
||||
canSeeMembersPage: {% if CAN_SEE_MEMBERS_PAGE %}true{% else %}false{% endif %},
|
||||
manageMedia: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
|
||||
manageUsers: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER %}true{% else %}false{% endif %},
|
||||
manageComments: {% if IS_MEDIACMS_ADMIN or IS_MEDIACMS_MANAGER or IS_MEDIACMS_EDITOR %}true{% else %}false{% endif %},
|
||||
usersNeedsToBeApproved: {% if USERS_NEEDS_TO_BE_APPROVED %}true{% else %}false{% endif %},
|
||||
},
|
||||
pages: {
|
||||
media: '/user/{{request.user.username}}',
|
||||
|
||||
@ -17,7 +17,7 @@ MediaCMS.contents = {
|
||||
}
|
||||
],
|
||||
belowNavMenu: null,
|
||||
footer: 'Powered by <a href="//mediacms.io" title="mediacms.io" target="_blank">mediacms.io</a>',
|
||||
footer: {% if SIDEBAR_FOOTER_TEXT %}'{{ SIDEBAR_FOOTER_TEXT|escapejs }}'{% else %}'Powered by <a href="//mediacms.io" title="mediacms.io" target="_blank">mediacms.io</a>'{% endif %},
|
||||
},
|
||||
uploader: {
|
||||
belowUploadArea: "{{PRE_UPLOAD_MEDIA_MESSAGE}}",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import User
|
||||
@ -5,20 +6,7 @@ from .models import User
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
search_fields = ["email", "username", "name"]
|
||||
exclude = (
|
||||
"user_permissions",
|
||||
"title",
|
||||
"password",
|
||||
"groups",
|
||||
"last_login",
|
||||
"is_featured",
|
||||
"location",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"media_count",
|
||||
"date_joined",
|
||||
"is_active",
|
||||
)
|
||||
exclude = ["user_permissions", "title", "password", "groups", "last_login", "is_featured", "location", "first_name", "last_name", "media_count", "date_joined", "is_active", "is_approved"]
|
||||
list_display = [
|
||||
"username",
|
||||
"name",
|
||||
@ -33,5 +21,10 @@ class UserAdmin(admin.ModelAdmin):
|
||||
list_filter = ["is_superuser", "is_editor", "is_manager"]
|
||||
ordering = ("-date_added",)
|
||||
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED:
|
||||
list_display.append("is_approved")
|
||||
list_filter.append("is_approved")
|
||||
exclude.remove("is_approved")
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from files.methods import is_mediacms_manager
|
||||
|
||||
@ -25,6 +26,7 @@ class UserForm(forms.ModelForm):
|
||||
"advancedUser",
|
||||
"is_manager",
|
||||
"is_editor",
|
||||
"is_approved",
|
||||
# "allow_contact",
|
||||
)
|
||||
|
||||
@ -44,6 +46,11 @@ class UserForm(forms.ModelForm):
|
||||
self.fields.pop("advancedUser")
|
||||
self.fields.pop("is_manager")
|
||||
self.fields.pop("is_editor")
|
||||
|
||||
if not settings.USERS_NEEDS_TO_BE_APPROVED or not is_mediacms_manager(user):
|
||||
if "is_approved" in self.fields:
|
||||
self.fields.pop("is_approved")
|
||||
|
||||
if user.socialaccount_set.exists():
|
||||
# for Social Accounts do not allow to edit the name
|
||||
self.fields["name"].widget.attrs['readonly'] = True
|
||||
|
||||
17
users/migrations/0002_user_is_approved.py
Normal file
17
users/migrations/0002_user_is_approved.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.6 on 2025-09-19 14:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='is_approved',
|
||||
field=models.BooleanField(blank=True, db_index=True, default=False, null=True, verbose_name='Is approved'),
|
||||
),
|
||||
]
|
||||
@ -29,6 +29,7 @@ class User(AbstractUser):
|
||||
name = models.CharField("full name", max_length=250, db_index=True)
|
||||
date_added = models.DateTimeField("date added", default=timezone.now, db_index=True)
|
||||
is_featured = models.BooleanField("Is featured", default=False, db_index=True)
|
||||
is_approved = models.BooleanField("Is approved", default=False, null=True, blank=True, db_index=True)
|
||||
|
||||
title = models.CharField("Title", max_length=250, blank=True)
|
||||
advancedUser = models.BooleanField("advanced user", default=False, db_index=True)
|
||||
|
||||
@ -22,7 +22,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
read_only_fields = (
|
||||
read_only_fields = [
|
||||
"date_added",
|
||||
"is_featured",
|
||||
"uid",
|
||||
@ -31,8 +31,8 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
"is_editor",
|
||||
"is_manager",
|
||||
"email_is_verified",
|
||||
)
|
||||
fields = (
|
||||
]
|
||||
fields = [
|
||||
"description",
|
||||
"date_added",
|
||||
"name",
|
||||
@ -45,7 +45,11 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
"is_editor",
|
||||
"is_manager",
|
||||
"email_is_verified",
|
||||
)
|
||||
]
|
||||
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED:
|
||||
fields.append("is_approved")
|
||||
read_only_fields.append("is_approved")
|
||||
|
||||
|
||||
class UserDetailSerializer(serializers.ModelSerializer):
|
||||
|
||||
@ -205,6 +205,12 @@ class UserList(APIView):
|
||||
operation_description='Paginated listing of users',
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
if settings.CAN_SEE_MEMBERS_PAGE == "editors" and not is_mediacms_editor(request.user):
|
||||
raise PermissionDenied("You do not have permission to view this page.")
|
||||
|
||||
if settings.CAN_SEE_MEMBERS_PAGE == "admins" and not request.user.is_superuser:
|
||||
raise PermissionDenied("You do not have permission to view this page.")
|
||||
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
paginator = pagination_class()
|
||||
users = User.objects.filter()
|
||||
@ -213,11 +219,57 @@ class UserList(APIView):
|
||||
if name:
|
||||
users = users.filter(Q(name__icontains=name) | Q(username__icontains=name))
|
||||
|
||||
if settings.USERS_NEEDS_TO_BE_APPROVED:
|
||||
is_approved = request.GET.get("is_approved")
|
||||
if is_approved == "true":
|
||||
users = users.filter(is_approved=True)
|
||||
elif is_approved == "false":
|
||||
users = users.filter(Q(is_approved=False) | Q(is_approved__isnull=True))
|
||||
|
||||
page = paginator.paginate_queryset(users, request)
|
||||
|
||||
serializer = UserSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=["username", "password", "email", "name"],
|
||||
properties={
|
||||
"username": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"password": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"email": openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL),
|
||||
"name": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
},
|
||||
),
|
||||
tags=["Users"],
|
||||
operation_summary="Create user",
|
||||
operation_description="Create a new user. Only for managers.",
|
||||
responses={201: UserSerializer},
|
||||
)
|
||||
def post(self, request, format=None):
|
||||
if not is_mediacms_manager(request.user):
|
||||
raise PermissionDenied("You do not have permission to create users.")
|
||||
|
||||
username = request.data.get("username")
|
||||
password = request.data.get("password")
|
||||
email = request.data.get("email")
|
||||
name = request.data.get("name")
|
||||
|
||||
if not all([username, password, email, name]):
|
||||
return Response({"detail": "username, password, email, and name are required."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if User.objects.filter(username=username).exists():
|
||||
return Response({"detail": "A user with that username already exists."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
return Response({"detail": "A user with that email already exists."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user = User.objects.create_user(username=username, password=password, email=email, name=name)
|
||||
|
||||
serializer = UserSerializer(user, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class UserDetail(APIView):
|
||||
""""""
|
||||
@ -284,27 +336,36 @@ class UserDetail(APIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[],
|
||||
manual_parameters=[
|
||||
openapi.Parameter(name='action', in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=True, description="action to perform ('change_password' or 'approve_user')"),
|
||||
openapi.Parameter(name='password', in_=openapi.IN_FORM, type=openapi.TYPE_STRING, required=False, description="new password (if action is 'change_password')"),
|
||||
],
|
||||
tags=['Users'],
|
||||
operation_summary='Xto_be_written',
|
||||
operation_description='to_be_written',
|
||||
operation_summary='Update user details',
|
||||
operation_description='Allows a user to change their password. Allows a manager to approve a user.',
|
||||
)
|
||||
def put(self, request, uid, format=None):
|
||||
# ADMIN
|
||||
user = self.get_user(uid)
|
||||
def put(self, request, username, format=None):
|
||||
user = self.get_user(username)
|
||||
if isinstance(user, Response):
|
||||
return user
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
action = request.data.get("action")
|
||||
if action == "feature":
|
||||
user.is_featured = True
|
||||
|
||||
if action == "change_password":
|
||||
# Permission to edit user is already checked by self.get_user -> self.check_object_permissions
|
||||
password = request.data.get("password")
|
||||
if not password:
|
||||
return Response({"detail": "Password is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
elif action == "unfeature":
|
||||
user.is_featured = False
|
||||
|
||||
elif action == "approve_user":
|
||||
if not is_mediacms_manager(request.user):
|
||||
raise PermissionDenied("You do not have permission to approve users.")
|
||||
user.is_approved = True
|
||||
user.save()
|
||||
else:
|
||||
return Response({"detail": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
serializer = UserDetailSerializer(user, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user