1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-19 15:38:38 +00:00

Org active (#7496)

* admin set org active/inactive

* update

* update

* update
This commit is contained in:
lian 2025-02-27 11:12:48 +08:00 committed by GitHub
parent ea3a7798b9
commit a7fb61a4d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 172 additions and 17 deletions

View File

@ -8,7 +8,7 @@ import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading'; import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator'; import Paginator from '../../../components/paginator';
import { systemAdminAPI } from '../../../utils/system-admin-api'; import { systemAdminAPI } from '../../../utils/system-admin-api';
import RoleSelector from '../../../components/single-selector'; import Selector from '../../../components/single-selector';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import UserLink from '../user-link'; import UserLink from '../user-link';
import toaster from '../../../components/toast'; import toaster from '../../../components/toast';
@ -52,8 +52,9 @@ class Content extends Component {
<table> <table>
<thead> <thead>
<tr> <tr>
<th width="20%">{gettext('Name')}</th> <th width="15%">{gettext('Name')}</th>
<th width="20%">{gettext('Creator')}</th> <th width="15%">{gettext('Creator')}</th>
<th width="10%">{gettext('Status')}</th>
<th width="20%">{gettext('Role')}</th> <th width="20%">{gettext('Role')}</th>
<th width="15%">{gettext('Space Used')}</th> <th width="15%">{gettext('Space Used')}</th>
<th width="20%">{gettext('Created At')}</th> <th width="20%">{gettext('Created At')}</th>
@ -66,6 +67,7 @@ class Content extends Component {
key={index} key={index}
item={item} item={item}
updateRole={this.props.updateRole} updateRole={this.props.updateRole}
updateStatus={this.props.updateStatus}
deleteOrg={this.props.deleteOrg} deleteOrg={this.props.deleteOrg}
isItemFreezed={this.state.isItemFreezed} isItemFreezed={this.state.isItemFreezed}
toggleItemFreezed={this.toggleItemFreezed} toggleItemFreezed={this.toggleItemFreezed}
@ -97,6 +99,7 @@ Content.propTypes = {
currentPage: PropTypes.number, currentPage: PropTypes.number,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
updateRole: PropTypes.func.isRequired, updateRole: PropTypes.func.isRequired,
updateStatus: PropTypes.func.isRequired,
deleteOrg: PropTypes.func.isRequired, deleteOrg: PropTypes.func.isRequired,
hasNextPage: PropTypes.bool, hasNextPage: PropTypes.bool,
resetPerPage: PropTypes.func, resetPerPage: PropTypes.func,
@ -111,6 +114,7 @@ class Item extends Component {
highlighted: false, highlighted: false,
isDeleteDialogOpen: false, isDeleteDialogOpen: false,
deleteDialogMsg: '', deleteDialogMsg: '',
isConfirmInactiveDialogOpen: false
}; };
} }
@ -148,6 +152,31 @@ class Item extends Component {
}); });
}; };
toggleConfirmInactiveDialog = () => {
this.setState({ isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen });
};
translateStatus = (status) => {
switch (status) {
case 'active':
return gettext('Active');
case 'inactive':
return gettext('Inactive');
}
};
updateStatus = (statusOption) => {
const isActive = statusOption.value == 'active';
if (isActive) {
toaster.notify(gettext('It may take some time, please wait.'));
}
this.props.updateStatus(this.props.item.org_id, isActive);
};
setOrgInactive = () => {
this.props.updateStatus(this.props.item.org_id, false);
};
translateRole = (role) => { translateRole = (role) => {
switch (role) { switch (role) {
case 'default': case 'default':
@ -170,7 +199,12 @@ class Item extends Component {
render() { render() {
const { item } = this.props; const { item } = this.props;
const { highlighted, isDeleteDialogOpen, deleteDialogMsg } = this.state; const {
highlighted,
isDeleteDialogOpen,
deleteDialogMsg,
isConfirmInactiveDialogOpen
} = this.state;
const { role: curRole } = item; const { role: curRole } = item;
this.roleOptions = availableRoles.map(item => { this.roleOptions = availableRoles.map(item => {
@ -182,6 +216,18 @@ class Item extends Component {
}); });
const currentSelectedOption = this.roleOptions.filter(item => item.isSelected)[0]; const currentSelectedOption = this.roleOptions.filter(item => item.isSelected)[0];
// edit status
const curStatus = item.is_active ? 'active' : 'inactive';
this.statusOptions = ['active', 'inactive'].map(item => {
return {
value: item,
text: this.translateStatus(item),
isSelected: item == curStatus
};
});
const currentSelectedStatusOption = this.statusOptions.filter(item => item.isSelected)[0];
const confirmSetUserInactiveMsg = gettext('Are you sure you want to set {user_placeholder} inactive?').replace('{user_placeholder}', item.org_name);
return ( return (
<Fragment> <Fragment>
<tr className={highlighted ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <tr className={highlighted ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
@ -190,7 +236,17 @@ class Item extends Component {
<UserLink email={item.creator_email} name={item.creator_name} /> <UserLink email={item.creator_email} name={item.creator_name} />
</td> </td>
<td> <td>
<RoleSelector <Selector
isDropdownToggleShown={highlighted}
currentSelectedOption={currentSelectedStatusOption}
options={this.statusOptions}
selectOption={this.updateStatus}
toggleItemFreezed={this.props.toggleItemFreezed}
operationBeforeSelect={item.is_active ? this.toggleConfirmInactiveDialog : undefined}
/>
</td>
<td>
<Selector
isDropdownToggleShown={highlighted} isDropdownToggleShown={highlighted}
currentSelectedOption={currentSelectedOption} currentSelectedOption={currentSelectedOption}
options={this.roleOptions} options={this.roleOptions}
@ -213,6 +269,15 @@ class Item extends Component {
toggleDialog={this.toggleDeleteDialog} toggleDialog={this.toggleDeleteDialog}
/> />
} }
{isConfirmInactiveDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Set organization inactive')}
message={confirmSetUserInactiveMsg}
executeOperation={this.setOrgInactive}
confirmBtnText={gettext('Set')}
toggleDialog={this.toggleConfirmInactiveDialog}
/>
}
</Fragment> </Fragment>
); );
} }
@ -221,6 +286,7 @@ class Item extends Component {
Item.propTypes = { Item.propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
updateRole: PropTypes.func.isRequired, updateRole: PropTypes.func.isRequired,
updateStatus: PropTypes.func.isRequired,
deleteOrg: PropTypes.func.isRequired, deleteOrg: PropTypes.func.isRequired,
isItemFreezed: PropTypes.bool.isRequired, isItemFreezed: PropTypes.bool.isRequired,
toggleItemFreezed: PropTypes.func.isRequired toggleItemFreezed: PropTypes.func.isRequired

View File

@ -66,6 +66,24 @@ class Orgs extends Component {
this.setState({ isAddOrgDialogOpen: !this.state.isAddOrgDialogOpen }); this.setState({ isAddOrgDialogOpen: !this.state.isAddOrgDialogOpen });
}; };
updateStatus = (orgID, isActive) => {
let orgInfo = {};
orgInfo.isActive = isActive;
systemAdminAPI.sysAdminUpdateOrg(orgID, orgInfo).then(res => {
let newOrgList = this.state.orgList.map(org => {
if (org.org_id == orgID) {
org.is_active = isActive;
}
return org;
});
this.setState({ orgList: newOrgList });
toaster.success(gettext('Edit succeeded'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateRole = (orgID, role) => { updateRole = (orgID, role) => {
let orgInfo = {}; let orgInfo = {};
orgInfo.role = role; orgInfo.role = role;
@ -143,6 +161,7 @@ class Orgs extends Component {
resetPerPage={this.resetPerPage} resetPerPage={this.resetPerPage}
getListByPage={this.getItemsByPage} getListByPage={this.getItemsByPage}
updateRole={this.updateRole} updateRole={this.updateRole}
updateStatus={this.updateStatus}
deleteOrg={this.deleteOrg} deleteOrg={this.deleteOrg}
/> />
</div> </div>

View File

@ -608,6 +608,9 @@ class SystemAdminAPI {
if (orgInfo.role) { if (orgInfo.role) {
formData.append('role', orgInfo.role); formData.append('role', orgInfo.role);
} }
if (orgInfo.isActive != undefined) {
formData.append('is_active', orgInfo.isActive);
}
return this.req.put(url, formData); return this.req.put(url, formData);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -29,7 +29,7 @@ from seahub.organizations.models import OrgSAMLConfig
try: try:
from seahub.settings import ORG_MEMBER_QUOTA_ENABLED from seahub.settings import ORG_MEMBER_QUOTA_ENABLED
except ImportError: except ImportError:
ORG_MEMBER_QUOTA_ENABLED= False ORG_MEMBER_QUOTA_ENABLED = False
if ORG_MEMBER_QUOTA_ENABLED: if ORG_MEMBER_QUOTA_ENABLED:
from seahub.organizations.models import OrgMemberQuota from seahub.organizations.models import OrgMemberQuota
@ -47,6 +47,7 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_org_info(org): def get_org_info(org):
org_id = org.org_id org_id = org.org_id
@ -56,6 +57,7 @@ def get_org_info(org):
org_info['ctime'] = timestamp_to_isoformat_timestr(org.ctime) org_info['ctime'] = timestamp_to_isoformat_timestr(org.ctime)
org_info['org_url_prefix'] = org.url_prefix org_info['org_url_prefix'] = org.url_prefix
org_info['role'] = OrgSettings.objects.get_role_by_org(org) org_info['role'] = OrgSettings.objects.get_role_by_org(org)
org_info['is_active'] = OrgSettings.objects.get_is_active_by_org(org)
creator = org.creator creator = org.creator
org_info['creator_email'] = creator org_info['creator_email'] = creator
@ -70,7 +72,9 @@ def get_org_info(org):
return org_info return org_info
def get_org_detailed_info(org): def get_org_detailed_info(org):
org_id = org.org_id org_id = org.org_id
org_info = get_org_info(org) org_info = get_org_info(org)
@ -99,6 +103,7 @@ def get_org_detailed_info(org):
return org_info return org_info
def gen_org_url_prefix(max_trial=None, length=20): def gen_org_url_prefix(max_trial=None, length=20):
"""Generate organization url prefix automatically. """Generate organization url prefix automatically.
If ``max_trial`` is large than 0, then re-try that times if failed. If ``max_trial`` is large than 0, then re-try that times if failed.
@ -234,7 +239,7 @@ class AdminOrganizations(APIView):
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
quota = request.data.get('quota', None) quota = request.data.get('quota', None)
if quota: if quota:
try: try:
@ -244,7 +249,7 @@ class AdminOrganizations(APIView):
except ValueError as e: except ValueError as e:
logger.error(e) logger.error(e)
return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid") return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid")
if ORG_MEMBER_QUOTA_ENABLED: if ORG_MEMBER_QUOTA_ENABLED:
member_limit = request.data.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT) member_limit = request.data.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT)
OrgMemberQuota.objects.set_quota(org_id, member_limit) OrgMemberQuota.objects.set_quota(org_id, member_limit)
@ -367,7 +372,6 @@ class AdminOrganization(APIView):
error_msg = 'quota invalid.' error_msg = 'quota invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
quota = quota_mb * get_file_size_unit('MB') quota = quota_mb * get_file_size_unit('MB')
try: try:
seafile_api.set_org_quota(org_id, quota) seafile_api.set_org_quota(org_id, quota)
@ -384,6 +388,15 @@ class AdminOrganization(APIView):
OrgSettings.objects.add_or_update(org, role) OrgSettings.objects.add_or_update(org, role)
is_active = request.data.get('is_active', None)
if is_active:
is_active = is_active.lower()
if is_active not in ('true', 'false'):
error_msg = 'is_active invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
OrgSettings.objects.add_or_update(org, is_active=is_active == 'true')
org = ccnet_api.get_org_by_id(org_id) org = ccnet_api.get_org_by_id(org_id)
org_info = get_org_info(org) org_info = get_org_info(org)
return Response(org_info) return Response(org_info)
@ -493,14 +506,14 @@ class AdminOrganizationsBaseInfo(APIView):
error_msg = 'Feature is not enabled.' error_msg = 'Feature is not enabled.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
org_ids = request.GET.getlist('org_ids',[]) org_ids = request.GET.getlist('org_ids', [])
orgs = [] orgs = []
for org_id in org_ids: for org_id in org_ids:
try: try:
org = ccnet_api.get_org_by_id(int(org_id)) org = ccnet_api.get_org_by_id(int(org_id))
if not org: if not org:
continue continue
except: except Exception:
continue continue
base_info = {'org_id': org.org_id, 'org_name': org.org_name} base_info = {'org_id': org.org_id, 'org_name': org.org_name}
orgs.append(base_info) orgs.append(base_info)

View File

@ -1,17 +1,23 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import re import re
from django.utils.deprecation import MiddlewareMixin from rest_framework import status
from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from django.core.cache import cache
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.translation import gettext as _
from django.utils.deprecation import MiddlewareMixin
from seaserv import ccnet_api from seaserv import ccnet_api
from seahub.auth import logout
from seahub.utils import render_error
from seahub.organizations.models import OrgSettings
from seahub.notifications.models import Notification from seahub.notifications.models import Notification
from seahub.notifications.utils import refresh_cache from seahub.notifications.utils import refresh_cache
from seahub.constants import DEFAULT_ADMIN from seahub.api2.utils import api_error
from seahub.settings import SITE_ROOT, SUPPORT_EMAIL
try: try:
from seahub.settings import CLOUD_MODE from seahub.settings import CLOUD_MODE
except ImportError: except ImportError:
@ -20,7 +26,7 @@ try:
from seahub.settings import MULTI_TENANCY from seahub.settings import MULTI_TENANCY
except ImportError: except ImportError:
MULTI_TENANCY = False MULTI_TENANCY = False
from seahub.settings import SITE_ROOT
class BaseMiddleware(MiddlewareMixin): class BaseMiddleware(MiddlewareMixin):
""" """
@ -38,6 +44,16 @@ class BaseMiddleware(MiddlewareMixin):
orgs = ccnet_api.get_orgs_by_user(username) orgs = ccnet_api.get_orgs_by_user(username)
if orgs: if orgs:
request.user.org = orgs[0] request.user.org = orgs[0]
if not OrgSettings.objects.get_is_active_by_org(request.user.org):
org_name = request.user.org.org_name
error_msg = _(f"Team {org_name} is inactive.")
if SUPPORT_EMAIL:
error_msg += " " + _(f"Please contact {SUPPORT_EMAIL} if you want to activate the team.")
logout(request)
if "api2/" in request.path or "api/v2.1/" in request.path:
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
return render_error(request, error_msg, {"organization_inactive": True})
else: else:
request.cloud_mode = False request.cloud_mode = False
@ -46,6 +62,7 @@ class BaseMiddleware(MiddlewareMixin):
def process_response(self, request, response): def process_response(self, request, response):
return response return response
class InfobarMiddleware(MiddlewareMixin): class InfobarMiddleware(MiddlewareMixin):
"""Query info bar close status, and store into request.""" """Query info bar close status, and store into request."""
@ -99,6 +116,7 @@ class ForcePasswdChangeMiddleware(MiddlewareMixin):
if self._request_in_black_list(request): if self._request_in_black_list(request):
return HttpResponseRedirect(reverse('auth_password_change')) return HttpResponseRedirect(reverse('auth_password_change'))
class UserAgentMiddleWare(MiddlewareMixin): class UserAgentMiddleWare(MiddlewareMixin):
user_agents_test_match = ( user_agents_test_match = (
"w3c ", "acs-", "alav", "alca", "amoi", "audi", "w3c ", "acs-", "alav", "alca", "amoi", "audi",
@ -151,7 +169,7 @@ class UserAgentMiddleWare(MiddlewareMixin):
# Test common mobile values. # Test common mobile values.
if self.user_agents_test_search_regex.search(user_agent) and \ if self.user_agents_test_search_regex.search(user_agent) and \
not self.user_agents_exception_search_regex.search(user_agent): not self.user_agents_exception_search_regex.search(user_agent):
is_mobile = True is_mobile = True
else: else:
# Nokia like test for WAP browsers. # Nokia like test for WAP browsers.

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-01-16 04:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='orgsettings',
name='is_active',
field=models.BooleanField(db_index=True, default=False),
),
]

View File

@ -18,7 +18,9 @@ FORCE_ADFS_LOGIN = 'force_adfs_login'
DISABLE_ORG_USER_CLEAN_TRASH = 'disable_org_user_clean_trash' DISABLE_ORG_USER_CLEAN_TRASH = 'disable_org_user_clean_trash'
DISABLE_ORG_ENCRYPTED_LIBRARY = 'disable_org_encrypted_library' DISABLE_ORG_ENCRYPTED_LIBRARY = 'disable_org_encrypted_library'
class OrgMemberQuotaManager(models.Manager): class OrgMemberQuotaManager(models.Manager):
def get_quota(self, org_id): def get_quota(self, org_id):
try: try:
return self.get(org_id=org_id).quota return self.get(org_id=org_id).quota
@ -63,7 +65,15 @@ class OrgSettingsManager(models.Manager):
logger.warning('Role %s is not valid' % role) logger.warning('Role %s is not valid' % role)
return DEFAULT_ORG return DEFAULT_ORG
def add_or_update(self, org, role=None): def get_is_active_by_org(self, org):
org_id = org.org_id
try:
is_active = self.get(org_id=org_id).is_active
return is_active
except OrgSettings.DoesNotExist:
return True
def add_or_update(self, org, role=None, is_active=None):
org_id = org.org_id org_id = org.org_id
try: try:
settings = self.get(org_id=org_id) settings = self.get(org_id=org_id)
@ -76,6 +86,9 @@ class OrgSettingsManager(models.Manager):
else: else:
logger.warning('Role %s is not valid' % role) logger.warning('Role %s is not valid' % role)
if is_active is not None:
settings.is_active = is_active
settings.save(using=self._db) settings.save(using=self._db)
return settings return settings
@ -83,6 +96,7 @@ class OrgSettingsManager(models.Manager):
class OrgSettings(models.Model): class OrgSettings(models.Model):
org_id = models.IntegerField(unique=True) org_id = models.IntegerField(unique=True)
role = models.CharField(max_length=100, null=True, blank=True) role = models.CharField(max_length=100, null=True, blank=True)
is_active = models.BooleanField(default=False, db_index=True)
objects = OrgSettingsManager() objects = OrgSettingsManager()

View File

@ -504,6 +504,7 @@ ENABLE_SHOW_ABOUT = True
# enable show wechat support # enable show wechat support
SHOW_WECHAT_SUPPORT_GROUP = False SHOW_WECHAT_SUPPORT_GROUP = False
SUPPORT_EMAIL = ''
# File preview # File preview
FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024 FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024

View File

@ -5,6 +5,9 @@
{% if unable_view_file %} {% if unable_view_file %}
<img src="{{MEDIA_URL}}img/failed-to-view-file.png" alt="" width="100" /> <img src="{{MEDIA_URL}}img/failed-to-view-file.png" alt="" width="100" />
<p class="unable-view-file-tip">{{ error_msg }}</p> <p class="unable-view-file-tip">{{ error_msg }}</p>
{% elif organization_inactive %}
<img src="{{MEDIA_URL}}img/organization-inactive.png" alt="" width="100" />
<p>{{ error_msg }}</p>
{% else %} {% else %}
<p class="error">{{ error_msg }}</p> <p class="error">{{ error_msg }}</p>
{% endif %} {% endif %}