Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

order orgs by quota usage #7345

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions frontend/src/pages/sys-admin/orgs/orgs-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,20 @@ class Content extends Component {
this.props.getListByPage(this.props.currentPage + 1);
};

sortByQuotaUsage = (e) => {
e.preventDefault();
this.props.sortByQuotaUsage();
};

render() {
const { loading, errorMsg, items } = this.props;
const { loading, errorMsg, items, sortBy, sortOrder } = this.props;
let sortIcon;
if (sortBy == '') {
// initial sort icon
sortIcon = <span className="sf3-font sf3-font-sort3"></span>;
} else {
sortIcon = <span className={`sf3-font ${sortOrder == 'asc' ? 'sf3-font-down rotate-180 d-inline-block' : 'sf3-font-down'}`}></span>;
}
if (loading) {
return <Loading />;
} else if (errorMsg) {
Expand All @@ -54,8 +66,10 @@ class Content extends Component {
<tr>
<th width="20%">{gettext('Name')}</th>
<th width="20%">{gettext('Creator')}</th>
<th width="20%">{gettext('Role')}</th>
<th width="15%">{gettext('Space Used')}</th>
<th width="15%">{gettext('Role')}</th>
<th width="20%">
<a className="d-inline-block table-sort-op" href="#" onClick={this.sortByQuotaUsage}>{gettext('Space Used')} {sortIcon}</a> / {gettext('Quota')}
</th>
<th width="20%">{gettext('Created At')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
Expand Down Expand Up @@ -94,6 +108,9 @@ Content.propTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
getListByPage: PropTypes.func.isRequired,
sortByQuotaUsage: PropTypes.func.isRequired,
sortOrder: PropTypes.string.isRequired,
sortBy: PropTypes.string.isRequired,
currentPage: PropTypes.number,
items: PropTypes.array.isRequired,
updateRole: PropTypes.func.isRequired,
Expand Down
27 changes: 25 additions & 2 deletions frontend/src/pages/sys-admin/orgs/orgs.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class Orgs extends Component {
loading: true,
errorMsg: '',
orgList: [],
sortBy: '',
sortOrder: 'asc',
currentPage: 1,
perPage: 100,
hasNextPage: false,
Expand All @@ -38,8 +40,8 @@ class Orgs extends Component {
}

getItemsByPage = (page) => {
const { perPage } = this.state;
systemAdminAPI.sysAdminListOrgs(page, perPage).then((res) => {
const { perPage, sortBy, sortOrder } = this.state;
systemAdminAPI.sysAdminListOrgs(page, perPage, sortBy, sortOrder).then((res) => {
this.setState({
loading: false,
orgList: res.data.organizations,
Expand Down Expand Up @@ -120,6 +122,24 @@ class Orgs extends Component {
navigate(`${siteRoot}sys/search-organizations/?query=${encodeURIComponent(keyword)}`);
};

sortByQuotaUsage = () => {
this.setState({
sortBy: 'quota_usage',
sortOrder: this.state.sortOrder == 'asc' ? 'desc' : 'asc',
currentPage: 1
}, () => {
let url = new URL(location.href);
let searchParams = new URLSearchParams(url.search);
const { currentPage, sortBy, sortOrder } = this.state;
searchParams.set('page', currentPage);
searchParams.set('order_by', sortBy);
searchParams.set('direction', sortOrder);
url.search = searchParams.toString();
navigate(url.toString());
this.getItemsByPage(1);
});
};

render() {
const { isAddOrgDialogOpen } = this.state;
return (
Expand All @@ -142,6 +162,9 @@ class Orgs extends Component {
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getItemsByPage}
sortByQuotaUsage={this.sortByQuotaUsage}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
updateRole={this.updateRole}
deleteOrg={this.deleteOrg}
/>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/utils/system-admin-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,12 +571,16 @@ class SystemAdminAPI {
return this.req.delete(url);
}

sysAdminListOrgs(page, perPage) {
sysAdminListOrgs(page, perPage, sortBy, sortOrder) {
const url = this.server + '/api/v2.1/admin/organizations/';
let params = {
page: page,
per_page: perPage
};
if (sortBy) {
params.order_by = sortBy;
params.direction = sortOrder;
}
return this.req.get(url, { params: params });
}

Expand Down
47 changes: 34 additions & 13 deletions seahub/api2/endpoints/admin/organizations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) 2012-2016 Seafile Ltd.
import logging
from types import SimpleNamespace

from django.utils.crypto import get_random_string

Expand All @@ -14,6 +15,7 @@
from seahub.auth.utils import get_virtual_id_by_email
from seahub.organizations.settings import ORG_MEMBER_QUOTA_DEFAULT
from seahub.utils import is_valid_email
from seahub.utils.db_api import SeafileDB
from seahub.utils.file_size import get_file_size_unit
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.base.templatetags.seahub_tags import email2nickname, \
Expand All @@ -29,7 +31,7 @@
try:
from seahub.settings import ORG_MEMBER_QUOTA_ENABLED
except ImportError:
ORG_MEMBER_QUOTA_ENABLED= False
ORG_MEMBER_QUOTA_ENABLED = False

if ORG_MEMBER_QUOTA_ENABLED:
from seahub.organizations.models import OrgMemberQuota
Expand All @@ -47,6 +49,7 @@

logger = logging.getLogger(__name__)


def get_org_info(org):
org_id = org.org_id

Expand All @@ -70,6 +73,7 @@ def get_org_info(org):

return org_info


def get_org_detailed_info(org):
org_id = org.org_id
org_info = get_org_info(org)
Expand Down Expand Up @@ -99,6 +103,7 @@ def get_org_detailed_info(org):

return org_info


def gen_org_url_prefix(max_trial=None, length=20):
"""Generate organization url prefix automatically.
If ``max_trial`` is large than 0, then re-try that times if failed.
Expand Down Expand Up @@ -163,19 +168,36 @@ def get(self, request):

start = (page - 1) * per_page

try:
orgs = ccnet_api.get_all_orgs(start, per_page)
total_count = ccnet_api.count_orgs()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
order_by = request.GET.get('order_by', '').lower().strip()
if not order_by:
try:
orgs = ccnet_api.get_all_orgs(start, per_page)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
else:
if order_by not in ('quota_usage'):
error_msg = 'order_by invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

direction = request.GET.get('direction', 'desc').lower().strip()
if direction not in ('asc', 'desc'):
error_msg = 'direction invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

seafile_db = SeafileDB()
org_dict_list = seafile_db.get_orgs_with_quota_usage(start, per_page, direction)
orgs = []
for org_dict in org_dict_list:
orgs.append(SimpleNamespace(**org_dict))

result = []
for org in orgs:
org_info = get_org_info(org)
result.append(org_info)

total_count = ccnet_api.count_orgs()
return Response({'organizations': result, 'total_count': total_count})

def post(self, request):
Expand Down Expand Up @@ -234,7 +256,7 @@ def post(self, request):
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)

quota = request.data.get('quota', None)
if quota:
try:
Expand All @@ -244,7 +266,7 @@ def post(self, request):
except ValueError as e:
logger.error(e)
return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid")

if ORG_MEMBER_QUOTA_ENABLED:
member_limit = request.data.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT)
OrgMemberQuota.objects.set_quota(org_id, member_limit)
Expand Down Expand Up @@ -367,7 +389,6 @@ def put(self, request, org_id):
error_msg = 'quota invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)


quota = quota_mb * get_file_size_unit('MB')
try:
seafile_api.set_org_quota(org_id, quota)
Expand Down Expand Up @@ -493,14 +514,14 @@ def get(self, request):
error_msg = 'Feature is not enabled.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)

org_ids = request.GET.getlist('org_ids',[])
org_ids = request.GET.getlist('org_ids', [])
orgs = []
for org_id in org_ids:
try:
org = ccnet_api.get_org_by_id(int(org_id))
if not org:
continue
except:
except Exception:
continue
base_info = {'org_id': org.org_id, 'org_name': org.org_name}
orgs.append(base_info)
Expand Down
68 changes: 61 additions & 7 deletions seahub/utils/db_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import configparser
from django.db import connection

from seahub.utils.ccnet_db import get_ccnet_db_name


class RepoTrash(object):

Expand All @@ -13,7 +15,9 @@ def __init__(self, **kwargs):
self.size = kwargs.get('size')
self.del_time = kwargs.get('del_time')


class WikiInfo(object):

def __init__(self, **kwargs):
self.repo_id = kwargs.get('repo_id')
self.wiki_name = kwargs.get('wiki_name')
Expand All @@ -25,12 +29,12 @@ def __init__(self, **kwargs):
self.last_modified = kwargs.get('last_modified')



class SeafileDB:

def __init__(self):

self.db_name = self._get_seafile_db_name()
self.ccnet_db_name = get_ccnet_db_name()

def _get_seafile_db_name(self):

Expand Down Expand Up @@ -390,7 +394,7 @@ def get_repo_ids_in_repo(self, repo_id):
for item in cursor.fetchall():
repo_id = item[0]
repo_ids.append(repo_id)
except:
except Exception:
return repo_ids

return repo_ids
Expand Down Expand Up @@ -452,7 +456,6 @@ def update_repo_user_shares(self, repo_id, new_owner, org_id=None):
with connection.cursor() as cursor:
cursor.execute(sql)


def update_repo_group_shares(self, repo_id, new_owner, org_id=None):
repo_ids = self.get_repo_ids_in_repo(repo_id)
repo_ids_str = ','.join(["'%s'" % str(rid) for rid in repo_ids])
Expand All @@ -467,14 +470,13 @@ def update_repo_group_shares(self, repo_id, new_owner, org_id=None):
with connection.cursor() as cursor:
cursor.execute(sql)


def delete_repo_user_token(self, repo_id, owner):
sql = f"""
DELETE FROM `{self.db_name}`.`RepoUserToken` where repo_id="{repo_id}" AND email="{owner}"
"""
with connection.cursor() as cursor:
cursor.execute(sql)

def get_all_wikis(self, start, limit, order_by):
order_by_size_sql = f"""
SELECT r.repo_id, i.name, o.owner_id, i.is_encrypted, s.size, i.status, c.file_count, i.update_time
Expand Down Expand Up @@ -516,12 +518,12 @@ def get_all_wikis(self, start, limit, order_by):
i.type = 'wiki'
LIMIT {limit} OFFSET {start}
"""

with connection.cursor() as cursor:
wikis = []
if order_by == 'size':
cursor.execute(order_by_size_sql)

elif order_by == 'file_count':
cursor.execute(order_by_filecount_sql)
else:
Expand All @@ -548,3 +550,55 @@ def get_all_wikis(self, start, limit, order_by):
wiki_info = WikiInfo(**params)
wikis.append(wiki_info)
return wikis

def get_orgs_with_quota_usage(self, offset=1, per_page=25, direction='asc'):

sql = f"""
SELECT
o.org_id,
o.org_name,
o.url_prefix,
o.creator,
o.ctime,
SUM(rs.size) AS quota_usage
FROM
`{self.ccnet_db_name}`.`Organization` o
LEFT JOIN
`{self.db_name}`.`OrgRepo` orp ON o.org_id = orp.org_id
LEFT JOIN
`{self.db_name}`.`RepoSize` rs ON orp.repo_id = rs.repo_id
LEFT JOIN
`{self.db_name}`.`VirtualRepo` vr ON rs.repo_id = vr.repo_id
WHERE
vr.repo_id IS NULL
GROUP BY
o.org_id, o.org_name
ORDER BY
quota_usage {direction}
LIMIT {per_page} OFFSET {offset};
"""

org_list = []
with connection.cursor() as cursor:

cursor.execute(sql)
for item in cursor.fetchall():

org_id = item[0]
org_name = item[1]
url_prefix = item[2]
creator = item[3]
ctime = item[4]
quota_usage = item[5]

org_info = {
'org_id': org_id,
'org_name': org_name,
'url_prefix': url_prefix,
'creator': creator,
'ctime': ctime,
'quota_usage': quota_usage
}
org_list.append(org_info)

return org_list
Loading