feat: approve users, edit users through manage users page (#1383)

This commit is contained in:
Markos Gogoulos 2025-09-20 15:16:52 +03:00 committed by GitHub
parent 8e8454d8c2
commit cbef629baf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1384 additions and 790 deletions

10
cms/auth_backends.py Normal file
View 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
View 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

View File

@ -128,6 +128,10 @@ USERS_CAN_SELF_REGISTER = True
RESTRICTED_DOMAINS_FOR_USER_REGISTRATION = ["xxx.com", "emaildomainwhatever.com"] 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"] # Comma separated list of domains: ["organization.com", "private.organization.com", "org2.com"]
# Empty list disables. # Empty list disables.
ALLOWED_DOMAINS_FOR_USER_REGISTRATION = [] ALLOWED_DOMAINS_FOR_USER_REGISTRATION = []
@ -501,6 +505,10 @@ ALLOW_CUSTOM_MEDIA_URLS = False
# Whether to allow anonymous users to list all users # Whether to allow anonymous users to list all users
ALLOW_ANONYMOUS_USER_LISTING = True 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 # Maximum number of media a user can upload
NUMBER_OF_MEDIA_USER_CAN_UPLOAD = 100 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 transcribe options - https://github.com/openai/whisper
WHISPER_MODEL = "base" 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: try:
# keep a local_settings.py file for local overrides # keep a local_settings.py file for local overrides
from .local_settings import * # noqa from .local_settings import * # noqa
@ -558,3 +569,12 @@ except ImportError:
if GLOBAL_LOGIN_REQUIRED: if GLOBAL_LOGIN_REQUIRED:
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware") auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
MIDDLEWARE.insert(auth_index + 1, "django.contrib.auth.middleware.LoginRequiredMiddleware") 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")

View File

@ -1 +1 @@
VERSION = "6.5.2" VERSION = "6.6.0"

View File

@ -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. 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 ## 6. Manage pages
to be written to be written

View File

@ -26,10 +26,22 @@ def stuff(request):
ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE ret["UPLOAD_MAX_SIZE"] = settings.UPLOAD_MAX_SIZE
ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER ret["UPLOAD_MAX_FILES_NUMBER"] = settings.UPLOAD_MAX_FILES_NUMBER
ret["PRE_UPLOAD_MEDIA_MESSAGE"] = settings.PRE_UPLOAD_MEDIA_MESSAGE 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["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_ADMIN"] = request.user.is_superuser
ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user) ret["IS_MEDIACMS_EDITOR"] = is_mediacms_editor(request.user)
ret["IS_MEDIACMS_MANAGER"] = is_mediacms_manager(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"] = settings.ALLOW_RATINGS
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY 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 ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE

View File

@ -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 import openapi as openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import status from rest_framework import status
@ -219,6 +221,13 @@ class UserList(APIView):
elif role == "editor": elif role == "editor":
qs = qs.filter(is_editor=True) 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}") users = qs.order_by(f"{ordering}{sort_by}")
paginator = pagination_class() paginator = pagination_class()

View File

@ -110,6 +110,9 @@ urlpatterns = [
re_path(r"^manage/users$", views.manage_users, name="manage_users"), re_path(r"^manage/users$", views.manage_users, name="manage_users"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + 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: if hasattr(settings, "USE_SAML") and settings.USE_SAML:
urlpatterns.append(re_path(r"^saml/metadata", views.saml_metadata, name="saml-metadata")) urlpatterns.append(re_path(r"^saml/metadata", views.saml_metadata, name="saml-metadata"))

View File

@ -1,4 +1,5 @@
# Import all views for backward compatibility # Import all views for backward compatibility
from .auth import custom_login_view, saml_metadata # noqa: F401 from .auth import custom_login_view, saml_metadata # noqa: F401
from .categories import CategoryList, TagList # noqa: F401 from .categories import CategoryList, TagList # noqa: F401
from .comments import CommentDetail, CommentList # 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 .media import MediaSearch # noqa: F401
from .pages import about # noqa: F401 from .pages import about # noqa: F401
from .pages import add_subtitle # 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 categories # noqa: F401
from .pages import contact # noqa: F401 from .pages import contact # noqa: F401
from .pages import edit_chapters # noqa: F401 from .pages import edit_chapters # noqa: F401

View File

@ -54,6 +54,11 @@ def about(request):
return render(request, "cms/about.html", context) 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): def setlanguage(request):
"""Set Language view""" """Set Language view"""
@ -517,6 +522,12 @@ def manage_comments(request):
def members(request): def members(request):
"""List members view""" """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 = {} context = {}
return render(request, "cms/members.html", context) return render(request, "cms/members.html", context)

View File

@ -1,7 +1,8 @@
import React, { useRef, useState, useEffect, useCallback } from 'react'; import React, { useRef, useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { usePopup } from '../../../utils/hooks/'; import { usePopup, useUser } from '../../../utils/hooks/';
import { PageStore } from '../../../utils/stores/'; import { PageStore } from '../../../utils/stores/';
import { csrfToken } from '../../../utils/helpers/';
import { PopupMain } from '../../_shared'; import { PopupMain } from '../../_shared';
import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon.jsx'; import { MaterialIcon } from '../../_shared/material-icon/MaterialIcon.jsx';
import { ManageItemDate } from './ManageMediaItem'; import { ManageItemDate } from './ManageMediaItem';
@ -38,34 +39,86 @@ function ManageItemUsername(props) {
return <i className="non-available">N/A</i>; return <i className="non-available">N/A</i>;
} }
function ManageItemCommentActions(props) { function ManageUsersItemActions(props) {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup(); const { userCan } = useUser();
const [isOpenPopup, setIsOpenPopup] = useState(false); const [deletePopupRef, DeletePopupContent, DeletePopupTrigger] = usePopup();
const [passwordPopupRef, PasswordPopupContent, PasswordPopupTrigger] = usePopup();
const [approvePopupRef, ApprovePopupContent, ApprovePopupTrigger] = usePopup();
function onPopupShow() { const [newPassword, setNewPassword] = useState('');
setIsOpenPopup(true);
}
function onPopupHide() { const [isDeleteOpen, setDeleteOpen] = useState(false);
setIsOpenPopup(false); const [isPasswordOpen, setPasswordOpen] = useState(false);
} const [isApproveOpen, setApproveOpen] = useState(false);
function onCancel() { function onProceedDelete() {
popupContentRef.current.tryToHide(); deletePopupRef.current.tryToHide();
if ('function' === typeof props.onCancel) {
props.onCancel();
}
}
function onProceed() {
popupContentRef.current.tryToHide();
if ('function' === typeof props.onProceed) { if ('function' === typeof props.onProceed) {
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 positionState = { updating: false, pending: 0 };
const onWindowResize = useCallback(function () { const onWindowResize = useCallback(function () {
if (positionState.updating) { if (positionState.updating) {
positionState.pending = positionState.pending + 1; positionState.pending = positionState.pending + 1;
@ -98,6 +151,8 @@ function ManageItemCommentActions(props) {
} }
}, []); }, []);
const isOpenPopup = isDeleteOpen || isPasswordOpen || isApproveOpen;
useEffect(() => { useEffect(() => {
if (isOpenPopup) { if (isOpenPopup) {
PageStore.on('window_scroll', onWindowResize); PageStore.on('window_scroll', onWindowResize);
@ -111,11 +166,94 @@ function ManageItemCommentActions(props) {
return ( return (
<div ref={props.containerRef} className="actions"> <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> <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> <PopupMain>
<div className="popup-message"> <div className="popup-message">
<span className="popup-message-title">Member removal</span> <span className="popup-message-title">Member removal</span>
@ -123,15 +261,15 @@ function ManageItemCommentActions(props) {
</div> </div>
<hr /> <hr />
<span className="popup-message-bottom"> <span className="popup-message-bottom">
<button className="button-link cancel-profile-removal" onClick={onCancel}> <button className="button-link cancel-profile-removal" onClick={onCancelDelete}>
CANCEL CANCEL
</button> </button>
<button className="button-link proceed-profile-removal" onClick={onProceed}> <button className="button-link proceed-profile-removal" onClick={onProceedDelete}>
PROCEED PROCEED
</button> </button>
</span> </span>
</PopupMain> </PopupMain>
</PopupContent> </DeletePopupContent>
</div> </div>
); );
} }
@ -168,10 +306,14 @@ export function ManageUsersItem(props) {
</div> </div>
<div className="mi-name"> <div className="mi-name">
<ManageItemName name={props.name} url={props.url} /> <ManageItemName name={props.name} url={props.url} />
<ManageItemCommentActions <ManageUsersItemActions
containerRef={actionsContainerRef} containerRef={actionsContainerRef}
name={props.name || props.username} name={props.name || props.username}
username={props.username}
is_approved={props.is_approved}
onProceed={onClickProceed} onProceed={onClickProceed}
onUserUpdate={props.onUserUpdate}
setMessage={props.setMessage}
/> />
</div> </div>
<div className="mi-username"> <div className="mi-username">
@ -213,6 +355,17 @@ export function ManageUsersItem(props) {
)} )}
</div> </div>
) : null} ) : 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"> <div className="mi-featured">
{void 0 === props.is_featured ? ( {void 0 === props.is_featured ? (
<i className="non-available">N/A</i> <i className="non-available">N/A</i>
@ -234,18 +387,25 @@ ManageUsersItem.propTypes = {
add_date: PropTypes.string, add_date: PropTypes.string,
is_featured: PropTypes.bool, is_featured: PropTypes.bool,
onCheckRow: PropTypes.func, onCheckRow: PropTypes.func,
onUserUpdate: PropTypes.func,
setMessage: PropTypes.func,
selectedRow: PropTypes.bool.isRequired, selectedRow: PropTypes.bool.isRequired,
hideDeleteAction: PropTypes.bool.isRequired, hideDeleteAction: PropTypes.bool.isRequired,
has_roles: PropTypes.bool, has_roles: PropTypes.bool,
has_verified: PropTypes.bool, has_verified: PropTypes.bool,
has_trusted: PropTypes.bool, has_trusted: PropTypes.bool,
has_approved: PropTypes.bool,
roles: PropTypes.array, roles: PropTypes.array,
is_verified: PropTypes.bool, is_verified: PropTypes.bool,
is_trusted: PropTypes.bool, is_trusted: PropTypes.bool,
is_approved: PropTypes.bool,
}; };
ManageUsersItem.defaultProps = { ManageUsersItem.defaultProps = {
has_roles: false, has_roles: false,
has_verified: false, has_verified: false,
has_trusted: false, has_trusted: false,
has_approved: false,
onUserUpdate: () => {},
setMessage: () => {},
}; };

View File

@ -45,6 +45,7 @@ export function ManageUsersItemHeader(props) {
{props.has_roles ? <div className="mi-role">Role</div> : null} {props.has_roles ? <div className="mi-role">Role</div> : null}
{props.has_verified ? <div className="mi-verified">Verified</div> : null} {props.has_verified ? <div className="mi-verified">Verified</div> : null}
{props.has_trusted ? <div className="mi-trusted">Trusted</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 className="mi-featured">Featured</div>
</div> </div>
); );
@ -59,10 +60,12 @@ ManageUsersItemHeader.propTypes = {
has_roles: PropTypes.bool, has_roles: PropTypes.bool,
has_verified: PropTypes.bool, has_verified: PropTypes.bool,
has_trusted: PropTypes.bool, has_trusted: PropTypes.bool,
has_approved: PropTypes.bool,
}; };
ManageUsersItemHeader.defaultProps = { ManageUsersItemHeader.defaultProps = {
has_roles: false, has_roles: false,
has_verified: false, has_verified: false,
has_trusted: false, has_trusted: false,
has_approved: false,
}; };

View File

@ -12,6 +12,89 @@ import { translateString } from '../../../utils/helpers/';
import './ManageItemList.scss'; 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) { function useManageItemList(props, itemsListRef) {
let previousItemsLength = 0; let previousItemsLength = 0;
@ -440,6 +523,35 @@ export function ManageItemList(props) {
onItemsLoad, onItemsLoad,
] = useManageItemListSync(props); ] = 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 [selectedItems, setSelectedItems] = useState([]);
const [selectedAllItems, setSelectedAllItems] = useState(false); const [selectedAllItems, setSelectedAllItems] = useState(false);
@ -592,50 +704,60 @@ export function ManageItemList(props) {
return () => { return () => {
if (listHandler) { if (listHandler) {
listHandler.cancelAll(); // listHandler.cancelAll();
setListHandler(null); setListHandler(null);
} }
}; };
}, []); }, [props.requestUrl]);
return !countedItems ? ( return !countedItems ? (
<PendingItemsList className={classname.listOuter} /> <PendingItemsList className={classname.listOuter} />
) : !items.length ? null : ( ) : (
<div className={classname.listOuter}> <div className={classname.listOuter}>
<ManageItemsOptions {message.text && (
totalItems={totalItems} <div className={`message ${message.type === 'error' ? 'error' : 'success'}`}>{message.text}</div>
pageItems={props.pageItems} )}
onPageButtonClick={onPageButtonClick} {'users' === props.manageType && <AddNewUser onUserAdded={refreshList} setMessage={setMessage} />}
query={parsedRequestUrlQuery || ''} {!items.length ? null : (
className="manage-items-options" <>
items={selectedItems} <ManageItemsOptions
pagesSize={listHandler.totalPages()} totalItems={totalItems}
onProceedRemoval={onBulkItemsRemoval} pageItems={props.pageItems}
/> onPageButtonClick={onPageButtonClick}
query={parsedRequestUrlQuery || ''}
className="manage-items-options"
items={selectedItems}
pagesSize={listHandler.totalPages()}
onProceedRemoval={onBulkItemsRemoval}
/>
<div ref={itemsListWrapperRef} className="items-list-wrap"> <div ref={itemsListWrapperRef} className="items-list-wrap">
<div ref={itemsListRef} className={classname.list}> <div ref={itemsListRef} className={classname.list}>
{renderManageItems(items, { {renderManageItems(items, {
...props, ...props,
onAllRowsCheck: onAllRowsCheck, onAllRowsCheck: onAllRowsCheck,
onRowCheck: onRowCheck, onRowCheck: onRowCheck,
selectedItems: selectedItems, selectedItems: selectedItems,
selectedAllItems: selectedAllItems, selectedAllItems: selectedAllItems,
onDelete: deleteItem, onDelete: deleteItem,
})} onUserUpdate: refreshList,
</div> setMessage: setMessage,
</div> })}
</div>
</div>
<ManageItemsOptions <ManageItemsOptions
totalItems={totalItems} totalItems={totalItems}
pageItems={props.pageItems} pageItems={props.pageItems}
onPageButtonClick={onPageButtonClick} onPageButtonClick={onPageButtonClick}
query={parsedRequestUrlQuery || ''} query={parsedRequestUrlQuery || ''}
className="manage-items-options popup-on-top" className="manage-items-options popup-on-top"
items={selectedItems} items={selectedItems}
pagesSize={listHandler.totalPages()} pagesSize={listHandler.totalPages()}
onProceedRemoval={onBulkItemsRemoval} onProceedRemoval={onBulkItemsRemoval}
/> />
</>
)}
</div> </div>
); );
} }

View File

@ -5,6 +5,7 @@ import { ManageCommentsItem } from '../../ManageItem/ManageCommentsItem';
import { ManageMediaItemHeader } from '../../ManageItem/ManageMediaItemHeader'; import { ManageMediaItemHeader } from '../../ManageItem/ManageMediaItemHeader';
import { ManageUsersItemHeader } from '../../ManageItem/ManageUsersItemHeader'; import { ManageUsersItemHeader } from '../../ManageItem/ManageUsersItemHeader';
import { ManageCommentsItemHeader } from '../../ManageItem/ManageCommentsItemHeader'; import { ManageCommentsItemHeader } from '../../ManageItem/ManageCommentsItemHeader';
import { useUser } from '../../../../utils/hooks/';
function useManageItem(props) { function useManageItem(props) {
const itemData = props.item; const itemData = props.item;
@ -15,6 +16,8 @@ function useManageItem(props) {
selectedRow: props.selectedRow, selectedRow: props.selectedRow,
onProceedRemoval: props.onProceedRemoval, onProceedRemoval: props.onProceedRemoval,
hideDeleteAction: props.hideDeleteAction, hideDeleteAction: props.hideDeleteAction,
onUserUpdate: props.onUserUpdate,
setMessage: props.setMessage,
}; };
return [itemData, itemProps]; return [itemData, itemProps];
@ -44,6 +47,7 @@ function ListManageMediaItem(props) {
} }
function ListManageUserItem(props) { function ListManageUserItem(props) {
const { userCan } = useUser();
const [itemData, itemProps] = useManageItem(props); const [itemData, itemProps] = useManageItem(props);
const roles = []; const roles = [];
@ -70,6 +74,8 @@ function ListManageUserItem(props) {
has_roles: void 0 !== itemData.is_editor || void 0 !== itemData.is_manager, has_roles: void 0 !== itemData.is_editor || void 0 !== itemData.is_manager,
has_verified: void 0 !== itemData.email_is_verified, has_verified: void 0 !== itemData.email_is_verified,
has_trusted: void 0 !== itemData.advancedUser, has_trusted: void 0 !== itemData.advancedUser,
is_approved: itemData.is_approved,
has_approved: userCan.usersNeedsToBeApproved && void 0 !== itemData.is_approved,
}; };
return <ManageUsersItem {...args} />; return <ManageUsersItem {...args} />;
@ -99,6 +105,8 @@ function ListManageItem(props) {
hideDeleteAction: false, hideDeleteAction: false,
onCheckRow: props.onCheckRow, onCheckRow: props.onCheckRow,
onProceedRemoval: props.onProceedRemoval, onProceedRemoval: props.onProceedRemoval,
onUserUpdate: props.onUserUpdate,
setMessage: props.setMessage,
}; };
if ('media' === props.type) { if ('media' === props.type) {
@ -117,6 +125,7 @@ function ListManageItem(props) {
} }
function ListManageItemHeader(props) { function ListManageItemHeader(props) {
const { userCan } = useUser();
const args = { const args = {
sort: props.sort, sort: props.sort,
order: props.order, 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); 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_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_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} />; return <ManageUsersItemHeader {...args} />;
} }

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PageStore } from '../../utils/stores/'; import { PageStore } from '../../utils/stores/';
import { useUser } from '../../utils/hooks/';
import { FilterOptions } from '../_shared'; import { FilterOptions } from '../_shared';
import './ManageItemList-filters.scss'; import './ManageItemList-filters.scss';
@ -11,12 +12,19 @@ const filters = {
{ id: 'editor', title: 'Editor' }, { id: 'editor', title: 'Editor' },
{ id: 'manager', title: 'Manager' }, { id: 'manager', title: 'Manager' },
], ],
approved: [
{ id: 'all', title: 'All' },
{ id: 'true', title: 'Yes' },
{ id: 'false', title: 'No' },
],
}; };
export function ManageUsersFilters(props) { export function ManageUsersFilters(props) {
const { userCan } = useUser();
const [isHidden, setIsHidden] = useState(props.hidden); const [isHidden, setIsHidden] = useState(props.hidden);
const [role, setFilterRole] = useState('all'); const [role, setFilterRole] = useState('all');
const [approved, setFilterApproved] = useState('all');
const containerRef = useRef(null); const containerRef = useRef(null);
const innerContainerRef = useRef(null); const innerContainerRef = useRef(null);
@ -30,6 +38,7 @@ export function ManageUsersFilters(props) {
function onFilterSelect(ev) { function onFilterSelect(ev) {
const args = { const args = {
role: role, role: role,
is_approved: approved,
}; };
switch (ev.currentTarget.getAttribute('filter')) { switch (ev.currentTarget.getAttribute('filter')) {
@ -38,6 +47,11 @@ export function ManageUsersFilters(props) {
props.onFiltersUpdate(args); props.onFiltersUpdate(args);
setFilterRole(args.role); setFilterRole(args.role);
break; 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} /> <FilterOptions id={'role'} options={filters.role} selected={role} onSelect={onFilterSelect} />
</div> </div>
</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>
</div> </div>
); );

View File

@ -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({ items.push({
link: links.members, link: links.members,
icon: 'people', icon: 'people',

View File

@ -14,6 +14,8 @@ export function init(user, features) {
register: true, register: true,
addMedia: false, addMedia: false,
editProfile: false, editProfile: false,
canSeeMembersPage: true,
usersNeedsToBeApproved: true,
changePassword: true, changePassword: true,
deleteProfile: false, deleteProfile: false,
readComment: true, 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.addMedia = true === user.can.addMedia;
MEMBER.can.editProfile = true === user.can.editProfile; MEMBER.can.editProfile = true === user.can.editProfile;
MEMBER.can.readComment = false === user.can.readComment ? false : true; 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

View 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 %}

View File

@ -16,9 +16,11 @@ MediaCMS.user = {
deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %}, deleteComment: {% if CAN_DELETE_COMMENTS %}true{% else %}false{% endif %},
editProfile: {% if CAN_EDIT %}true{% else %}false{% endif %}, editProfile: {% if CAN_EDIT %}true{% else %}false{% endif %},
deleteProfile: {% if CAN_DELETE %}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 %}, 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 %}, 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 %}, 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: { pages: {
media: '/user/{{request.user.username}}', media: '/user/{{request.user.username}}',

View File

@ -1,26 +1,26 @@
MediaCMS.contents = { MediaCMS.contents = {
sidebar: { sidebar: {
navMenuItems: [{ navMenuItems: [{
text: "About", text: "About",
link: "/about", link: "/about",
icon: 'contact_support', icon: 'contact_support',
}, },
{ {
text: "Terms", text: "Terms",
link: "/tos", link: "/tos",
icon: 'insert_drive_file', icon: 'insert_drive_file',
}, },
{ {
text: "Contact", text: "Contact",
link: "/contact", link: "/contact",
icon: 'alternate_email', icon: 'alternate_email',
} }
], ],
belowNavMenu: null, 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: { uploader: {
belowUploadArea: "{{PRE_UPLOAD_MEDIA_MESSAGE}}", belowUploadArea: "{{PRE_UPLOAD_MEDIA_MESSAGE}}",
postUploadMessage: "{{POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY}}", postUploadMessage: "{{POST_UPLOAD_AUTHOR_MESSAGE_UNLISTED_NO_COMMENTARY}}",
}, },
}; };

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from .models import User from .models import User
@ -5,20 +6,7 @@ from .models import User
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
search_fields = ["email", "username", "name"] search_fields = ["email", "username", "name"]
exclude = ( exclude = ["user_permissions", "title", "password", "groups", "last_login", "is_featured", "location", "first_name", "last_name", "media_count", "date_joined", "is_active", "is_approved"]
"user_permissions",
"title",
"password",
"groups",
"last_login",
"is_featured",
"location",
"first_name",
"last_name",
"media_count",
"date_joined",
"is_active",
)
list_display = [ list_display = [
"username", "username",
"name", "name",
@ -33,5 +21,10 @@ class UserAdmin(admin.ModelAdmin):
list_filter = ["is_superuser", "is_editor", "is_manager"] list_filter = ["is_superuser", "is_editor", "is_manager"]
ordering = ("-date_added",) 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) admin.site.register(User, UserAdmin)

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.conf import settings
from files.methods import is_mediacms_manager from files.methods import is_mediacms_manager
@ -25,6 +26,7 @@ class UserForm(forms.ModelForm):
"advancedUser", "advancedUser",
"is_manager", "is_manager",
"is_editor", "is_editor",
"is_approved",
# "allow_contact", # "allow_contact",
) )
@ -44,6 +46,11 @@ class UserForm(forms.ModelForm):
self.fields.pop("advancedUser") self.fields.pop("advancedUser")
self.fields.pop("is_manager") self.fields.pop("is_manager")
self.fields.pop("is_editor") 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(): if user.socialaccount_set.exists():
# for Social Accounts do not allow to edit the name # for Social Accounts do not allow to edit the name
self.fields["name"].widget.attrs['readonly'] = True self.fields["name"].widget.attrs['readonly'] = True

View 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'),
),
]

View File

@ -29,6 +29,7 @@ class User(AbstractUser):
name = models.CharField("full name", max_length=250, db_index=True) name = models.CharField("full name", max_length=250, db_index=True)
date_added = models.DateTimeField("date added", default=timezone.now, 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_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) title = models.CharField("Title", max_length=250, blank=True)
advancedUser = models.BooleanField("advanced user", default=False, db_index=True) advancedUser = models.BooleanField("advanced user", default=False, db_index=True)

View File

@ -22,7 +22,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
read_only_fields = ( read_only_fields = [
"date_added", "date_added",
"is_featured", "is_featured",
"uid", "uid",
@ -31,8 +31,8 @@ class UserSerializer(serializers.ModelSerializer):
"is_editor", "is_editor",
"is_manager", "is_manager",
"email_is_verified", "email_is_verified",
) ]
fields = ( fields = [
"description", "description",
"date_added", "date_added",
"name", "name",
@ -45,7 +45,11 @@ class UserSerializer(serializers.ModelSerializer):
"is_editor", "is_editor",
"is_manager", "is_manager",
"email_is_verified", "email_is_verified",
) ]
if settings.USERS_NEEDS_TO_BE_APPROVED:
fields.append("is_approved")
read_only_fields.append("is_approved")
class UserDetailSerializer(serializers.ModelSerializer): class UserDetailSerializer(serializers.ModelSerializer):

View File

@ -205,6 +205,12 @@ class UserList(APIView):
operation_description='Paginated listing of users', operation_description='Paginated listing of users',
) )
def get(self, request, format=None): 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 pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class() paginator = pagination_class()
users = User.objects.filter() users = User.objects.filter()
@ -213,11 +219,57 @@ class UserList(APIView):
if name: if name:
users = users.filter(Q(name__icontains=name) | Q(username__icontains=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) page = paginator.paginate_queryset(users, request)
serializer = UserSerializer(page, many=True, context={"request": request}) serializer = UserSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data) 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): class UserDetail(APIView):
"""""" """"""
@ -284,27 +336,36 @@ class UserDetail(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema( @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'], tags=['Users'],
operation_summary='Xto_be_written', operation_summary='Update user details',
operation_description='to_be_written', operation_description='Allows a user to change their password. Allows a manager to approve a user.',
) )
def put(self, request, uid, format=None): def put(self, request, username, format=None):
# ADMIN user = self.get_user(username)
user = self.get_user(uid)
if isinstance(user, Response): if isinstance(user, Response):
return user return user
if not request.user.is_superuser:
return Response({"detail": "not allowed"}, status=status.HTTP_400_BAD_REQUEST)
action = request.data.get("action") 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() 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() user.save()
else:
return Response({"detail": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST)
serializer = UserDetailSerializer(user, context={"request": request}) serializer = UserDetailSerializer(user, context={"request": request})
return Response(serializer.data) return Response(serializer.data)