mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 18:30:53 +00:00
org admin update name/logo (#5385)
This commit is contained in:
@@ -12,6 +12,7 @@ import OrgStatisticReport from './statistic/statistic-reports';
|
||||
import OrgDesktopDevices from './devices/desktop-devices.js';
|
||||
import OrgMobileDevices from './devices/mobile-devices.js';
|
||||
import OrgDevicesErrors from './devices/devices-errors.js';
|
||||
import WebSettings from './web-settings/web-settings';
|
||||
import OrgUsers from './org-users-users';
|
||||
import OrgUsersSearchUsers from './org-users-search-users';
|
||||
import OrgAdmins from './org-users-admins';
|
||||
@@ -93,6 +94,7 @@ class Org extends React.Component {
|
||||
<OrgDesktopDevices path={siteRoot + 'org/deviceadmin/desktop-devices/'} />
|
||||
<OrgMobileDevices path={siteRoot + 'org/deviceadmin/mobile-devices/'} />
|
||||
<OrgDevicesErrors path={siteRoot + 'org/deviceadmin/devices-errors/'} />
|
||||
<WebSettings path={siteRoot + 'org/web-settings'} />
|
||||
<OrgUsers path={siteRoot + 'org/useradmin'} />
|
||||
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
|
||||
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />
|
||||
|
@@ -50,6 +50,12 @@ class SidePanel extends React.Component {
|
||||
<span className="nav-text">{gettext('Devices')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className={`nav-link ellipsis ${this.getActiveClass('web-settings')}`} to={siteRoot + 'org/web-settings/'} onClick={() => this.tabItemClick('web-settings')} >
|
||||
<span className="sf2-icon-cog2"></span>
|
||||
<span className="nav-text">{gettext('Settings')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className={`nav-link ellipsis ${this.getActiveClass('repoadmin')}`} to={siteRoot + 'org/repoadmin/'} onClick={() => this.tabItemClick('repoadmin')} >
|
||||
<span className="sf2-icon-library"></span>
|
||||
|
51
frontend/src/pages/org-admin/web-settings/checkbox-item.js
Normal file
51
frontend/src/pages/org-admin/web-settings/checkbox-item.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Input } from 'reactstrap';
|
||||
import SettingItemBase from './setting-item-base';
|
||||
|
||||
const propTypes = {
|
||||
saveSetting: PropTypes.func.isRequired,
|
||||
keyText: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
|
||||
helpTip: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class WebSettingCheckbox extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
inputChecked: this.props.value
|
||||
};
|
||||
}
|
||||
|
||||
onInputChange = (e) => {
|
||||
const checked = e.target.checked;
|
||||
const valueToNum = checked ? 1 : 0;
|
||||
this.setState({
|
||||
inputChecked: checked
|
||||
});
|
||||
this.props.saveSetting(this.props.keyText, valueToNum);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputChecked } = this.state;
|
||||
const { helpTip, displayName } = this.props;
|
||||
return (
|
||||
<SettingItemBase
|
||||
displayName={displayName}
|
||||
mainContent={
|
||||
<Fragment>
|
||||
<Input className="ml-0" checked={inputChecked} type='checkbox' onChange={this.onInputChange} />
|
||||
<p className="ml-4">{helpTip}</p>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebSettingCheckbox.propTypes = propTypes;
|
||||
|
||||
export default WebSettingCheckbox;
|
58
frontend/src/pages/org-admin/web-settings/file-item.js
Normal file
58
frontend/src/pages/org-admin/web-settings/file-item.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import SettingItemBase from './setting-item-base';
|
||||
|
||||
const propTypes = {
|
||||
postFile: PropTypes.func.isRequired,
|
||||
keyText: PropTypes.string.isRequired,
|
||||
filePath: PropTypes.string.isRequired,
|
||||
helpTip: PropTypes.string.isRequired,
|
||||
fileWidth: PropTypes.number.isRequired,
|
||||
fileHeight: PropTypes.number.isRequired,
|
||||
displayName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class WebSettingFile extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.fileInput = React.createRef();
|
||||
}
|
||||
|
||||
uploadFile = () => {
|
||||
if (!this.fileInput.current.files.length) {
|
||||
return;
|
||||
}
|
||||
const file = this.fileInput.current.files[0];
|
||||
this.props.postFile(file, this.props.keyText);
|
||||
}
|
||||
|
||||
openFileInput = () => {
|
||||
this.fileInput.current.click();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { helpTip, filePath, fileWidth, fileHeight, displayName } = this.props;
|
||||
return (
|
||||
<SettingItemBase
|
||||
displayName={displayName}
|
||||
helpTip={helpTip}
|
||||
mainContent={
|
||||
<img src={filePath + '?t=' + new Date().getTime()} alt={displayName} width={fileWidth} height={fileHeight} className="mb-1" />
|
||||
}
|
||||
extraContent={
|
||||
<Fragment>
|
||||
<Button color="secondary" onClick={this.openFileInput}>{gettext('Change')}</Button>
|
||||
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInput} />
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebSettingFile.propTypes = propTypes;
|
||||
|
||||
export default WebSettingFile;
|
79
frontend/src/pages/org-admin/web-settings/input-item.js
Normal file
79
frontend/src/pages/org-admin/web-settings/input-item.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Input, Button } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import SettingItemBase from './setting-item-base';
|
||||
|
||||
const propTypes = {
|
||||
inputType: PropTypes.string,
|
||||
saveSetting: PropTypes.func.isRequired,
|
||||
keyText: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string,PropTypes.number]),
|
||||
helpTip: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
class WebSettingInput extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isBtnsShown: false,
|
||||
value: this.props.value
|
||||
};
|
||||
}
|
||||
|
||||
toggleBtns = () => {
|
||||
this.setState({isBtnsShown: !this.state.isBtnsShown});
|
||||
}
|
||||
|
||||
hideBtns = (e) => {
|
||||
if (!this.state.isBtnsShown) {
|
||||
return;
|
||||
}
|
||||
if (this.props.value != this.state.value) {
|
||||
this.setState({value: this.props.value});
|
||||
}
|
||||
this.toggleBtns();
|
||||
}
|
||||
|
||||
onInputChange = (e) => {
|
||||
this.setState({ value: e.target.value });
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
const value = this.state.value.trim();
|
||||
if (value != this.props.value) {
|
||||
this.props.saveSetting(this.props.keyText, value);
|
||||
}
|
||||
this.toggleBtns();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isBtnsShown, value } = this.state;
|
||||
const { helpTip, displayName, inputType, disabled } = this.props;
|
||||
return (
|
||||
<SettingItemBase
|
||||
displayName={displayName}
|
||||
helpTip={helpTip}
|
||||
mainContent={
|
||||
disabled ?
|
||||
<Input type={inputType || 'text'} className={inputType == 'textarea' ? 'web-setting-textarea' : ''} value={value} disabled /> :
|
||||
<Input type={inputType || 'text'} className={inputType == 'textarea' ? 'web-setting-textarea' : ''} onChange={this.onInputChange} onFocus={this.toggleBtns} onBlur={this.hideBtns} value={value} />
|
||||
}
|
||||
extraContent={
|
||||
isBtnsShown ?
|
||||
<Fragment>
|
||||
<Button className="sf2-icon-tick web-setting-icon-btn web-setting-icon-btn-submit" onMouseDown={this.onSubmit} title={gettext('Submit')}></Button>
|
||||
<Button className="ml-1 sf2-icon-x2 web-setting-icon-btn web-setting-icon-btn-cancel" title={gettext('Cancel')}></Button>
|
||||
</Fragment> : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebSettingInput.propTypes = propTypes;
|
||||
|
||||
export default WebSettingInput;
|
28
frontend/src/pages/org-admin/web-settings/section.js
Normal file
28
frontend/src/pages/org-admin/web-settings/section.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
headingText: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
class Section extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { headingText, children} = this.props;
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{headingText}</h4>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Section.propTypes = propTypes;
|
||||
|
||||
export default Section;
|
@@ -0,0 +1,41 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, Label } from 'reactstrap';
|
||||
|
||||
const propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
helpTip: PropTypes.string,
|
||||
mainContent: PropTypes.object.isRequired,
|
||||
extraContent: PropTypes.object
|
||||
};
|
||||
|
||||
class SettingItemBase extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { helpTip, displayName, mainContent, extraContent } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<Row className="my-4">
|
||||
<Col md="3">
|
||||
<Label className="web-setting-label">{displayName}</Label>
|
||||
</Col>
|
||||
<Col md="5">
|
||||
{mainContent}
|
||||
{helpTip && <p className="small text-secondary mt-1">{helpTip}</p>}
|
||||
</Col>
|
||||
<Col md="4">
|
||||
{extraContent}
|
||||
</Col>
|
||||
</Row>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingItemBase.propTypes = propTypes;
|
||||
|
||||
export default SettingItemBase;
|
114
frontend/src/pages/org-admin/web-settings/web-settings.js
Normal file
114
frontend/src/pages/org-admin/web-settings/web-settings.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, isPro, mediaUrl, logoPath, orgID, orgEnableAdminCustomLogo, orgEnableAdminCustomName } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
import Section from './section';
|
||||
import InputItem from './input-item';
|
||||
import FileItem from './file-item';
|
||||
import CheckboxItem from './checkbox-item';
|
||||
|
||||
import '../../../css/system-admin-web-settings.css';
|
||||
|
||||
class WebSettings extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
config_dict: null,
|
||||
logoPath: mediaUrl + logoPath,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
seafileAPI.orgAdminGetOrgInfo().then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
config_dict: res.data
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateName= (key, newOrgName) => {
|
||||
seafileAPI.orgAdminUpdateName(orgID, newOrgName).then((res) => {
|
||||
this.setState({
|
||||
config_dict: res.data
|
||||
});
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
updateLogo = (file) => {
|
||||
seafileAPI.orgAdminUpdateLogo(orgID, file).then((res) => {
|
||||
this.setState({
|
||||
logoPath: mediaUrl + res.data.logo_path
|
||||
});
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, config_dict, logoPath } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar {...this.props} />
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading">{gettext('Settings')}</h3>
|
||||
</div>
|
||||
<div className="cur-view-content container mw-100">
|
||||
{loading && <Loading />}
|
||||
{errorMsg && <p className="error text-center mt-4">{errorMsg}</p>}
|
||||
{(!loading && !errorMsg) && config_dict &&
|
||||
<Fragment>
|
||||
<p className="small text-secondary my-4"></p>
|
||||
|
||||
<Section headingText={gettext('Info')}>
|
||||
<Fragment>
|
||||
<InputItem
|
||||
saveSetting={this.updateName}
|
||||
displayName={gettext('Team name')}
|
||||
keyText='orgName'
|
||||
value={config_dict['org_name']}
|
||||
helpTip={''}
|
||||
disabled={!orgEnableAdminCustomName}
|
||||
/>
|
||||
{ orgEnableAdminCustomLogo && <FileItem
|
||||
postFile={this.updateLogo}
|
||||
displayName='Logo'
|
||||
keyText='Logo'
|
||||
filePath={logoPath}
|
||||
fileWidth={256}
|
||||
fileHeight={64}
|
||||
helpTip='logo.png, 256px * 64px'
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
</Section>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebSettings;
|
@@ -133,8 +133,11 @@ export const filePermission = window.draft ? window.draft.config.perm : '';
|
||||
|
||||
// org admin
|
||||
export const orgID = window.org ? window.org.pageOptions.orgID : '';
|
||||
export const orgName = window.org ? window.org.pageOptions.orgName : '';
|
||||
export const invitationLink = window.org ? window.org.pageOptions.invitationLink : '';
|
||||
export const orgMemberQuotaEnabled = window.org ? window.org.pageOptions.orgMemberQuotaEnabled : '';
|
||||
export const orgEnableAdminCustomLogo = window.org ? window.org.pageOptions.orgEnableAdminCustomLogo === 'True' : false;
|
||||
export const orgEnableAdminCustomName = window.org ? window.org.pageOptions.orgEnableAdminCustomName === 'True' : false;
|
||||
|
||||
// sys admin
|
||||
export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : '';
|
||||
|
@@ -26,6 +26,8 @@ from seahub.settings import SEAFILE_VERSION, SITE_DESCRIPTION, \
|
||||
CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \
|
||||
PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK
|
||||
|
||||
from seahub.organizations.models import OrgAdminSettings
|
||||
from seahub.organizations.settings import ORG_ENABLE_ADMIN_CUSTOM_LOGO
|
||||
from seahub.onlyoffice.settings import ENABLE_ONLYOFFICE, ONLYOFFICE_CONVERTER_EXTENSIONS
|
||||
from seahub.constants import DEFAULT_ADMIN
|
||||
from seahub.utils import get_site_name, get_service_url
|
||||
@@ -89,6 +91,11 @@ def base(request):
|
||||
if os.path.exists(custom_logo_file):
|
||||
logo_path = CUSTOM_LOGO_PATH
|
||||
|
||||
if ORG_ENABLE_ADMIN_CUSTOM_LOGO and org:
|
||||
org_logo_url = OrgAdminSettings.objects.get_org_logo_url(org.org_id)
|
||||
if org_logo_url:
|
||||
logo_path = org_logo_url
|
||||
|
||||
# get favicon path
|
||||
custom_favicon_file = os.path.join(MEDIA_ROOT, CUSTOM_FAVICON_PATH)
|
||||
if os.path.exists(custom_favicon_file):
|
||||
|
@@ -1,34 +1,29 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import logging
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from seaserv import ccnet_api, seafile_api
|
||||
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.api2.permissions import IsProVersion
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
|
||||
from seahub.organizations.models import OrgMemberQuota
|
||||
from seahub.organizations.settings import ORG_MEMBER_QUOTA_ENABLED
|
||||
from seahub.organizations.settings import ORG_MEMBER_QUOTA_ENABLED, \
|
||||
ORG_ENABLE_ADMIN_CUSTOM_NAME
|
||||
from seahub.organizations.api.permissions import IsOrgAdmin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OrgAdminInfo(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
permission_classes = (IsProVersion, IsOrgAdmin)
|
||||
|
||||
def get(self, request):
|
||||
"""Get info of an organization
|
||||
"""
|
||||
|
||||
org = request.user.org
|
||||
org_id = org.org_id
|
||||
def get_org_info(request, org_id):
|
||||
|
||||
# space quota
|
||||
try:
|
||||
@@ -65,12 +60,50 @@ class OrgAdminInfo(APIView):
|
||||
active_members = len([m for m in org_members if m.is_active])
|
||||
|
||||
info = {}
|
||||
info['org_id'] = org_id
|
||||
info['org_name'] = org.org_name
|
||||
info['storage_quota'] = storage_quota
|
||||
info['storage_usage'] = storage_usage
|
||||
info['member_quota'] = member_quota
|
||||
info['member_usage'] = member_usage
|
||||
info['active_members'] = active_members
|
||||
|
||||
return info
|
||||
|
||||
|
||||
class OrgAdminInfo(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
permission_classes = (IsProVersion, IsOrgAdmin)
|
||||
|
||||
def get(self, request):
|
||||
"""Get info of an organization
|
||||
"""
|
||||
|
||||
org = request.user.org
|
||||
org_id = org.org_id
|
||||
|
||||
info = get_org_info(request, org_id)
|
||||
info['org_id'] = org_id
|
||||
info['org_name'] = org.org_name
|
||||
|
||||
return Response(info)
|
||||
|
||||
def put(self, request):
|
||||
"""Update info of an organization
|
||||
"""
|
||||
|
||||
new_name = request.data.get('org_name', None)
|
||||
if new_name:
|
||||
|
||||
if not ORG_ENABLE_ADMIN_CUSTOM_NAME:
|
||||
error_msg = _('Feature is not enabled.')
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
org_id = request.user.org.org_id
|
||||
ccnet_api.set_org_name(org_id, new_name)
|
||||
|
||||
info = get_org_info(request, org_id)
|
||||
info['org_id'] = org_id
|
||||
info['org_name'] = new_name
|
||||
|
||||
return Response(info)
|
||||
|
74
seahub/organizations/api/admin/logo.py
Normal file
74
seahub/organizations/api/admin/logo.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
|
||||
from seahub.api2.permissions import IsProVersion, IsOrgAdminUser
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.utils import api_error
|
||||
|
||||
from seahub.avatar.settings import AVATAR_ALLOWED_FILE_EXTS, AVATAR_MAX_SIZE
|
||||
from seahub.organizations.models import OrgAdminSettings
|
||||
from seahub.organizations.settings import ORG_ENABLE_ADMIN_CUSTOM_LOGO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrgAdminLogo(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||
|
||||
def get(self, request, org_id):
|
||||
|
||||
if not ORG_ENABLE_ADMIN_CUSTOM_LOGO:
|
||||
error_msg = _('Feature is not enabled.')
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
org_logo_url = OrgAdminSettings.objects.get_org_logo_url(org_id)
|
||||
return Response({'logo_path': org_logo_url})
|
||||
|
||||
def post(self, request, org_id):
|
||||
|
||||
if not ORG_ENABLE_ADMIN_CUSTOM_LOGO:
|
||||
error_msg = _('Feature is not enabled.')
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
image_file = request.FILES.get('file', None)
|
||||
if not image_file:
|
||||
error_msg = 'file invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
(root, ext) = os.path.splitext(image_file.name.lower())
|
||||
if AVATAR_ALLOWED_FILE_EXTS and ext not in AVATAR_ALLOWED_FILE_EXTS:
|
||||
error_msg_dict = {
|
||||
'ext': ext,
|
||||
'valid_exts_list': ", ".join(AVATAR_ALLOWED_FILE_EXTS)
|
||||
}
|
||||
error_msg = _("%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s") % error_msg_dict
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
if image_file.size > AVATAR_MAX_SIZE:
|
||||
error_msg_dict = {
|
||||
'size': filesizeformat(image_file.size),
|
||||
'max_valid_size': filesizeformat(AVATAR_MAX_SIZE)
|
||||
}
|
||||
error_msg = _("Your file is too big (%(size)s), the maximum allowed size is %(max_valid_size)s") % error_msg_dict
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
try:
|
||||
OrgAdminSettings.objects.save_org_logo(org_id, image_file)
|
||||
org_logo_url = OrgAdminSettings.objects.get_org_logo_url(org_id)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||
|
||||
return Response({'logo_path': org_logo_url})
|
@@ -21,6 +21,7 @@ from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgA
|
||||
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
|
||||
|
||||
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
|
||||
from .api.admin.logo import OrgAdminLogo
|
||||
|
||||
from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \
|
||||
OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \
|
||||
@@ -65,6 +66,7 @@ urlpatterns = [
|
||||
OrgUrlPrefixView.as_view(),
|
||||
name='api-v2.1-org-admin-url-prefix'),
|
||||
|
||||
url(r'^(?P<org_id>\d+)/admin/logo/$', OrgAdminLogo.as_view(), name='api-v2.1-org-admin-logo'),
|
||||
url(r'^(?P<org_id>\d+)/admin/devices/$', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'),
|
||||
url(r'^(?P<org_id>\d+)/admin/devices-errors/$', OrgAdminDevicesErrors.as_view(), name='api-v2.1-org-admin-devices-errors'),
|
||||
|
||||
|
@@ -1,10 +1,15 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from django.db import models
|
||||
|
||||
from .settings import ORG_MEMBER_QUOTA_DEFAULT
|
||||
|
||||
from seahub.constants import DEFAULT_ORG
|
||||
from seahub.role_permissions.utils import get_available_roles
|
||||
from seahub.avatar.util import get_avatar_file_storage
|
||||
from seahub.avatar.settings import AVATAR_STORAGE_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -130,3 +135,63 @@ class OrgSAMLConfig(models.Model):
|
||||
'single_logout_service': self.single_logout_service,
|
||||
'valid_days': self.valid_days,
|
||||
}
|
||||
|
||||
|
||||
def _gen_org_logo_path(org_id, image_file):
|
||||
|
||||
(root, ext) = os.path.splitext(image_file.name.lower())
|
||||
return '%s/org-logo/%s/%s%s' % (AVATAR_STORAGE_DIR, org_id, uuid.uuid4(), ext)
|
||||
|
||||
|
||||
def _save_org_logo_file(org_id, image_file):
|
||||
|
||||
org_logo_path = _gen_org_logo_path(org_id, image_file)
|
||||
storage = get_avatar_file_storage()
|
||||
storage.save(org_logo_path, image_file)
|
||||
|
||||
return org_logo_path
|
||||
|
||||
|
||||
def _delete_org_logo_file(org_logo_path):
|
||||
|
||||
storage = get_avatar_file_storage()
|
||||
storage.delete(org_logo_path)
|
||||
|
||||
|
||||
class OrgAdminSettingsManager(models.Manager):
|
||||
|
||||
def save_org_logo(self, org_id, image_file):
|
||||
|
||||
obj = self.filter(org_id=org_id, key='org_logo_path').first()
|
||||
if obj and obj.value: # delete old file
|
||||
_delete_org_logo_file(obj.value)
|
||||
if not obj:
|
||||
obj = self.model(org_id=org_id, key='org_logo_path')
|
||||
|
||||
obj.value = _save_org_logo_file(org_id, image_file)
|
||||
obj.save()
|
||||
|
||||
return obj
|
||||
|
||||
def get_org_logo_url(self, org_id):
|
||||
|
||||
obj = self.filter(org_id=org_id, key='org_logo_path').first()
|
||||
if not obj:
|
||||
return ''
|
||||
|
||||
return obj.value
|
||||
|
||||
|
||||
class OrgAdminSettings(models.Model):
|
||||
|
||||
# boolean settings / str settings / int settings, etc
|
||||
# key: default-value
|
||||
|
||||
org_id = models.IntegerField(db_index=True, null=False)
|
||||
key = models.CharField(max_length=255, null=False)
|
||||
value = models.TextField()
|
||||
|
||||
objects = OrgAdminSettingsManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'key')]
|
||||
|
@@ -14,3 +14,6 @@ ORG_TRIAL_DAYS = getattr(settings, 'ORG_TRIAL_DAYS', -1)
|
||||
ORG_AUTO_URL_PREFIX = getattr(settings, 'ORG_AUTO_URL_PREFIX', True)
|
||||
|
||||
ORG_ENABLE_ADMIN_INVITE_USER = getattr(settings, 'ORG_ENABLE_ADMIN_INVITE_USER', False)
|
||||
|
||||
ORG_ENABLE_ADMIN_CUSTOM_NAME = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_NAME', True)
|
||||
ORG_ENABLE_ADMIN_CUSTOM_LOGO = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_LOGO', False)
|
||||
|
@@ -13,7 +13,9 @@
|
||||
orgName: '{{ org.org_name|escapejs }}',
|
||||
groupID: '{{ group_id }}',
|
||||
invitationLink: '{{ invitation_link|escapejs }}',
|
||||
orgMemberQuotaEnabled: '{{ org_member_quota_enabled }}'
|
||||
orgMemberQuotaEnabled: '{{ org_member_quota_enabled }}',
|
||||
orgEnableAdminCustomLogo: '{{ org_enable_admin_custom_logo }}',
|
||||
orgEnableAdminCustomName: '{{ org_enable_admin_custom_name }}'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -16,6 +16,7 @@ urlpatterns = [
|
||||
url(r'^deviceadmin/mobile-devices/$', react_fake_view, name='org_device_admin_mobile_devices'),
|
||||
url(r'^deviceadmin/devices-errors/$', react_fake_view, name='org_device_admin_devices_errors'),
|
||||
|
||||
url(r'^web-settings/$', react_fake_view, name='org_web_settings'),
|
||||
url(r'^useradmin/$', react_fake_view, name='org_user_admin'),
|
||||
url(r'^useradmin/search-users/$', react_fake_view, name='org_user_admin_search_users'),
|
||||
url(r'^useradmin/admins/$', react_fake_view, name='org_useradmin_admins'),
|
||||
|
@@ -30,7 +30,8 @@ from seahub.organizations.signals import org_created
|
||||
from seahub.organizations.decorators import org_staff_required
|
||||
from seahub.organizations.forms import OrgRegistrationForm
|
||||
from seahub.organizations.settings import ORG_AUTO_URL_PREFIX, \
|
||||
ORG_MEMBER_QUOTA_ENABLED, ORG_ENABLE_ADMIN_INVITE_USER
|
||||
ORG_MEMBER_QUOTA_ENABLED, ORG_ENABLE_ADMIN_INVITE_USER, \
|
||||
ORG_ENABLE_ADMIN_CUSTOM_LOGO, ORG_ENABLE_ADMIN_CUSTOM_NAME
|
||||
from seahub.organizations.utils import get_or_create_invitation_link
|
||||
|
||||
# Get an instance of a logger
|
||||
@@ -248,6 +249,8 @@ def react_fake_view(request, **kwargs):
|
||||
return render(request, "organizations/org_admin_react.html", {
|
||||
'org': org,
|
||||
'org_member_quota_enabled': ORG_MEMBER_QUOTA_ENABLED,
|
||||
'org_enable_admin_custom_logo': ORG_ENABLE_ADMIN_CUSTOM_LOGO,
|
||||
'org_enable_admin_custom_name': ORG_ENABLE_ADMIN_CUSTOM_NAME,
|
||||
'group_id': group_id,
|
||||
'invitation_link': invitation_link,
|
||||
})
|
||||
|
Reference in New Issue
Block a user