1
0
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:
lian
2023-03-07 18:11:33 +08:00
committed by GitHub
parent 8d05178828
commit 0f8542e894
18 changed files with 615 additions and 43 deletions

View File

@@ -12,6 +12,7 @@ import OrgStatisticReport from './statistic/statistic-reports';
import OrgDesktopDevices from './devices/desktop-devices.js'; import OrgDesktopDevices from './devices/desktop-devices.js';
import OrgMobileDevices from './devices/mobile-devices.js'; import OrgMobileDevices from './devices/mobile-devices.js';
import OrgDevicesErrors from './devices/devices-errors.js'; import OrgDevicesErrors from './devices/devices-errors.js';
import WebSettings from './web-settings/web-settings';
import OrgUsers from './org-users-users'; import OrgUsers from './org-users-users';
import OrgUsersSearchUsers from './org-users-search-users'; import OrgUsersSearchUsers from './org-users-search-users';
import OrgAdmins from './org-users-admins'; import OrgAdmins from './org-users-admins';
@@ -93,6 +94,7 @@ class Org extends React.Component {
<OrgDesktopDevices path={siteRoot + 'org/deviceadmin/desktop-devices/'} /> <OrgDesktopDevices path={siteRoot + 'org/deviceadmin/desktop-devices/'} />
<OrgMobileDevices path={siteRoot + 'org/deviceadmin/mobile-devices/'} /> <OrgMobileDevices path={siteRoot + 'org/deviceadmin/mobile-devices/'} />
<OrgDevicesErrors path={siteRoot + 'org/deviceadmin/devices-errors/'} /> <OrgDevicesErrors path={siteRoot + 'org/deviceadmin/devices-errors/'} />
<WebSettings path={siteRoot + 'org/web-settings'} />
<OrgUsers path={siteRoot + 'org/useradmin'} /> <OrgUsers path={siteRoot + 'org/useradmin'} />
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} /> <OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} /> <OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />

View File

@@ -50,6 +50,12 @@ class SidePanel extends React.Component {
<span className="nav-text">{gettext('Devices')}</span> <span className="nav-text">{gettext('Devices')}</span>
</Link> </Link>
</li> </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"> <li className="nav-item">
<Link className={`nav-link ellipsis ${this.getActiveClass('repoadmin')}`} to={siteRoot + 'org/repoadmin/'} onClick={() => this.tabItemClick('repoadmin')} > <Link className={`nav-link ellipsis ${this.getActiveClass('repoadmin')}`} to={siteRoot + 'org/repoadmin/'} onClick={() => this.tabItemClick('repoadmin')} >
<span className="sf2-icon-library"></span> <span className="sf2-icon-library"></span>

View 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;

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -133,8 +133,11 @@ export const filePermission = window.draft ? window.draft.config.perm : '';
// org admin // org admin
export const orgID = window.org ? window.org.pageOptions.orgID : ''; 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 invitationLink = window.org ? window.org.pageOptions.invitationLink : '';
export const orgMemberQuotaEnabled = window.org ? window.org.pageOptions.orgMemberQuotaEnabled : ''; 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 // sys admin
export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : ''; export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : '';

View File

@@ -26,6 +26,8 @@ from seahub.settings import SEAFILE_VERSION, SITE_DESCRIPTION, \
CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \ CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \
PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK 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.onlyoffice.settings import ENABLE_ONLYOFFICE, ONLYOFFICE_CONVERTER_EXTENSIONS
from seahub.constants import DEFAULT_ADMIN from seahub.constants import DEFAULT_ADMIN
from seahub.utils import get_site_name, get_service_url from seahub.utils import get_site_name, get_service_url
@@ -89,6 +91,11 @@ def base(request):
if os.path.exists(custom_logo_file): if os.path.exists(custom_logo_file):
logo_path = CUSTOM_LOGO_PATH 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 # get favicon path
custom_favicon_file = os.path.join(MEDIA_ROOT, CUSTOM_FAVICON_PATH) custom_favicon_file = os.path.join(MEDIA_ROOT, CUSTOM_FAVICON_PATH)
if os.path.exists(custom_favicon_file): if os.path.exists(custom_favicon_file):

View File

@@ -1,22 +1,74 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import logging import logging
from rest_framework import status
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from django.utils.translation import ugettext as _
from seaserv import ccnet_api, seafile_api from seaserv import ccnet_api, seafile_api
from seahub.api2.utils import api_error
from seahub.api2.permissions import IsProVersion from seahub.api2.permissions import IsProVersion
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.organizations.models import OrgMemberQuota 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 from seahub.organizations.api.permissions import IsOrgAdmin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_org_info(request, org_id):
# space quota
try:
storage_quota = seafile_api.get_org_quota(org_id)
except Exception as e:
logger.error(e)
storage_quota = 0
# storage usage
try:
storage_usage = seafile_api.get_org_quota_usage(org_id)
except Exception as e:
logger.error(e)
storage_usage = 0
# member quota
if ORG_MEMBER_QUOTA_ENABLED:
member_quota = OrgMemberQuota.objects.get_quota(org_id)
else:
member_quota = None
# member usage
try:
url_prefix = request.user.org.url_prefix
org_members = ccnet_api.get_org_emailusers(url_prefix, -1, -1)
except Exception as e:
logger.error(e)
org_members = []
member_usage = 0
active_members = 0
if org_members:
member_usage = len(org_members)
active_members = len([m for m in org_members if m.is_active])
info = {}
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): class OrgAdminInfo(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
@@ -30,47 +82,28 @@ class OrgAdminInfo(APIView):
org = request.user.org org = request.user.org
org_id = org.org_id org_id = org.org_id
# space quota info = get_org_info(request, org_id)
try:
storage_quota = seafile_api.get_org_quota(org_id)
except Exception as e:
logger.error(e)
storage_quota = 0
# storage usage
try:
storage_usage = seafile_api.get_org_quota_usage(org_id)
except Exception as e:
logger.error(e)
storage_usage = 0
# member quota
if ORG_MEMBER_QUOTA_ENABLED:
member_quota = OrgMemberQuota.objects.get_quota(org_id)
else:
member_quota = None
# member usage
try:
url_prefix = request.user.org.url_prefix
org_members = ccnet_api.get_org_emailusers(url_prefix, -1, -1)
except Exception as e:
logger.error(e)
org_members = []
member_usage = 0
active_members = 0
if org_members:
member_usage = len(org_members)
active_members = len([m for m in org_members if m.is_active])
info = {}
info['org_id'] = org_id info['org_id'] = org_id
info['org_name'] = org.org_name info['org_name'] = org.org_name
info['storage_quota'] = storage_quota
info['storage_usage'] = storage_usage return Response(info)
info['member_quota'] = member_quota
info['member_usage'] = member_usage def put(self, request):
info['active_members'] = active_members """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) return Response(info)

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

View File

@@ -21,6 +21,7 @@ from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgA
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
from .api.admin.logo import OrgAdminLogo
from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \ from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \
OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \ OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \
@@ -65,6 +66,7 @@ urlpatterns = [
OrgUrlPrefixView.as_view(), OrgUrlPrefixView.as_view(),
name='api-v2.1-org-admin-url-prefix'), 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/$', 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'), url(r'^(?P<org_id>\d+)/admin/devices-errors/$', OrgAdminDevicesErrors.as_view(), name='api-v2.1-org-admin-devices-errors'),

View File

@@ -1,10 +1,15 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import os
import uuid
import logging import logging
from django.db import models from django.db import models
from .settings import ORG_MEMBER_QUOTA_DEFAULT from .settings import ORG_MEMBER_QUOTA_DEFAULT
from seahub.constants import DEFAULT_ORG from seahub.constants import DEFAULT_ORG
from seahub.role_permissions.utils import get_available_roles 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__) logger = logging.getLogger(__name__)
@@ -130,3 +135,63 @@ class OrgSAMLConfig(models.Model):
'single_logout_service': self.single_logout_service, 'single_logout_service': self.single_logout_service,
'valid_days': self.valid_days, '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')]

View File

@@ -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_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_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)

View File

@@ -13,7 +13,9 @@
orgName: '{{ org.org_name|escapejs }}', orgName: '{{ org.org_name|escapejs }}',
groupID: '{{ group_id }}', groupID: '{{ group_id }}',
invitationLink: '{{ invitation_link|escapejs }}', 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> </script>

View File

@@ -16,6 +16,7 @@ urlpatterns = [
url(r'^deviceadmin/mobile-devices/$', react_fake_view, name='org_device_admin_mobile_devices'), 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'^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/$', react_fake_view, name='org_user_admin'),
url(r'^useradmin/search-users/$', react_fake_view, name='org_user_admin_search_users'), 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'), url(r'^useradmin/admins/$', react_fake_view, name='org_useradmin_admins'),

View File

@@ -30,7 +30,8 @@ from seahub.organizations.signals import org_created
from seahub.organizations.decorators import org_staff_required from seahub.organizations.decorators import org_staff_required
from seahub.organizations.forms import OrgRegistrationForm from seahub.organizations.forms import OrgRegistrationForm
from seahub.organizations.settings import ORG_AUTO_URL_PREFIX, \ 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 from seahub.organizations.utils import get_or_create_invitation_link
# Get an instance of a logger # Get an instance of a logger
@@ -248,6 +249,8 @@ def react_fake_view(request, **kwargs):
return render(request, "organizations/org_admin_react.html", { return render(request, "organizations/org_admin_react.html", {
'org': org, 'org': org,
'org_member_quota_enabled': ORG_MEMBER_QUOTA_ENABLED, '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, 'group_id': group_id,
'invitation_link': invitation_link, 'invitation_link': invitation_link,
}) })