1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-28 03:10:45 +00:00

Subscription (#6069)

* subscription api

* subscription page

* subscriptionAPI.js

* buy quota

* add currency icons

---------

Co-authored-by: Michael An <2331806369@qq.com>
This commit is contained in:
欢乐马 2024-05-17 11:14:55 +08:00 committed by GitHub
parent 533bacef1b
commit 6d1bb9039d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1377 additions and 5 deletions

View File

@ -38,6 +38,7 @@ const entryFiles = {
sysAdmin: '/pages/sys-admin',
search: '/pages/search',
uploadLink: '/pages/upload-link',
subscription: '/subscription.js',
};
const getEntries = (isEnvDevelopment) => {

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715912574269" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13472" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M643.2 467.2l-217.6-64c-25.6-6.4-41.6-32-41.6-57.6 0-32 25.6-57.6 57.6-57.6H576c25.6 0 48 6.4 67.2 22.4 12.8 9.6 28.8 6.4 38.4-3.2l70.4-67.2c16-16 12.8-38.4-3.2-51.2C700.8 150.4 640 128 576 128V32c0-19.2-16-32-32-32h-64c-19.2 0-32 12.8-32 32v96h-6.4c-128 0-230.4 108.8-217.6 240 9.6 92.8 80 166.4 166.4 192l204.8 60.8c25.6 6.4 41.6 32 41.6 57.6 0 32-25.6 57.6-57.6 57.6H448c-25.6 0-48-6.4-67.2-22.4-12.8-9.6-28.8-6.4-38.4 3.2l-70.4 67.2c-16 16-12.8 38.4 3.2 51.2C323.2 873.6 384 896 448 896v96c0 16 16 32 32 32h64c19.2 0 32-16 32-32v-96c92.8-3.2 179.2-57.6 211.2-144 44.8-124.8-28.8-252.8-144-284.8z" fill="#949494" p-id="13473"></path></svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@ -6,6 +6,10 @@ import { seafileAPI } from '../../utils/seafile-api';
import { siteRoot, gettext, appAvatarURL, enableSSOToThirdpartWebsite } from '../../utils/constants';
import toaster from '../toast';
const {
isOrgContext,
} = window.app.pageOptions;
const propTypes = {
isAdminPanel: PropTypes.bool,
};
@ -22,6 +26,7 @@ class Account extends Component {
isStaff: false,
isOrgStaff: false,
usageRate: '',
enableSubscription: false,
};
this.isFirstMounted = true;
}
@ -81,6 +86,7 @@ class Account extends Component {
isInstAdmin: resp.data.is_inst_admin,
isOrgStaff: resp.data.is_org_staff === 1 ? true : false,
showInfo: !this.state.showInfo,
enableSubscription: resp.data.enable_subscription,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
@ -163,6 +169,7 @@ class Account extends Component {
</div>
</div>
<a href={siteRoot + 'profile/'} className="item">{gettext('Settings')}</a>
{(this.state.enableSubscription && !isOrgContext) && <a href={siteRoot + 'subscription/'} className="item">{'付费管理'}</a>}
{this.renderMenu()}
{enableSSOToThirdpartWebsite && <a href={siteRoot + 'sso-to-thirdpart/'} className="item">{gettext('Customer Portal')}</a>}
<a href={siteRoot + 'accounts/logout/'} className="item">{gettext('Log out')}</a>

View File

@ -0,0 +1,523 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import toaster from './toast';
import { Modal, ModalHeader, ModalBody, ModalFooter, InputGroup, InputGroupAddon, InputGroupText, Input, Button } from 'reactstrap';
import { gettext, serviceURL } from '../utils/constants';
import { Utils } from '../utils/utils';
import { subscriptionAPI } from '../utils/subscription-api';
import Loading from './loading';
import '../css/layout.css';
import '../css/subscription.css';
const {
isOrgContext,
} = window.app.pageOptions;
const PlansPropTypes = {
plans: PropTypes.array.isRequired,
onPay: PropTypes.func.isRequired,
paymentType: PropTypes.string.isRequired,
handleContentScroll: PropTypes.func.isRequired,
};
class Plans extends Component {
constructor(props) {
super(props);
this.state = {
currentPlan: props.plans[0],
assetQuotaUnitCount: 1,
count: 1,
};
}
togglePlan = (plan) => {
this.setState({currentPlan: plan}, () => {
});
};
onPay = () => {
let { paymentType } = this.props;
let { currentPlan, assetQuotaUnitCount, count } = this.state;
let totalAmount, assetQuota, newUserCount;
// parse
if (paymentType === 'paid') {
newUserCount = currentPlan.count;
totalAmount = currentPlan.total_amount;
} else if (paymentType === 'extend_time') {
newUserCount = currentPlan.count;
assetQuota = currentPlan.asset_quota;
totalAmount = currentPlan.total_amount;
} else if (paymentType === 'add_user') {
newUserCount = count;
totalAmount = count * currentPlan.price_per_user;
} else if (paymentType === 'buy_quota') {
assetQuota = (assetQuotaUnitCount) * currentPlan.asset_quota_unit;
totalAmount = assetQuotaUnitCount * currentPlan.price_per_asset_quota_unit;
} else {
toaster.danger(gettext('Internal Server Error.'));
return;
}
this.props.onPay(currentPlan.plan_id, newUserCount, assetQuota, totalAmount);
};
onCountInputChange = (e) => {
let { currentPlan } = this.state;
if (!currentPlan.can_custom_count) {
return;
}
let count = e.target.value.replace(/^(0+)|[^\d]+/g, '');
if (count < 1) {
count = 1;
} else if (count > 9999) {
count = 9999;
}
this.setState({count: count});
};
onAssetQuotaUnitCountInputChange = (e) => {
let { currentPlan } = this.state;
if (!currentPlan.can_custom_asset_quota) {
return;
}
let count = e.target.value.replace(/^(0+)|[^\d]+/g, '');
if (count < 1) {
count = 1;
} else if (count > 9999) {
count = 9999;
}
this.setState({assetQuotaUnitCount: count});
};
renderPaidOrExtendTime = () => {
let { plans, paymentType } = this.props;
let { currentPlan } = this.state;
let boughtQuota = 0;
if (paymentType === 'extend_time') {
boughtQuota = currentPlan.asset_quota - 100;
}
let totalAmount = currentPlan.total_amount;
let originalTotalAmount = totalAmount;
return (
<div className='d-flex flex-column subscription-container'>
<span className="subscription-subtitle">{'选择方案'}</span>
<dl className='items-dl'>
{plans.map((item, index) => {
let selectedCss = item.plan_id === currentPlan.plan_id ? 'plan-selected' : '';
let countDescription = '¥' + item.price_per_user;
if (isOrgContext) {
countDescription += '/每用户';
}
return (
<dd key={index} className={`plan-description-item ${selectedCss}`} onClick={this.togglePlan.bind(this, item)}>
<span className='plan-name'>{item.name}</span>
<span className='plan-description'>{countDescription}</span>
</dd>
);
})}
</dl>
{paymentType === 'extend_time' && boughtQuota > 0 &&
<Fragment>
<span className="subscription-subtitle">{'增加空间'}</span>
<dl className='items-dl'>
<dd className='order-item order-item-top order-item-bottom subscription-list'>
<span className='order-into'>{currentPlan.asset_quota_unit + 'GB x ' + (boughtQuota / currentPlan.asset_quota_unit)}</span>
{/* 续费时候需要减去附赠的100GB */}
<span className='order-value'>{'¥' + (boughtQuota / currentPlan.asset_quota_unit) * currentPlan.price_per_asset_quota_unit}</span>
</dd>
</dl>
</Fragment>
}
<span className="subscription-subtitle">{'方案汇总'}</span>
<dl className='items-dl'>
<div>
<dd className='order-item order-item-top'>
<span className='order-into'>{'所选方案'}</span>
<span className='order-value'>{currentPlan.name}</span>
</dd>
{isOrgContext &&
<dd className='order-item'>
<span className='order-into'>{'成员人数'}</span>
<span className='order-value'>{currentPlan.count + '人'}</span>
</dd>
}
<dd className='order-item'>
<span className='order-into'>{'可用空间'}</span>
<span className='order-value'>{'100GB(附赠)' + (boughtQuota > 0 ? '+' + boughtQuota + 'GB(扩充)' : '')}</span>
</dd>
<dd className='order-item order-item-bottom rounded-0'>
<span className='order-into'>{'到期时间'}</span>
<span className='order-value'>{currentPlan.new_term_end}</span>
</dd>
<dd className='order-item order-item-bottom subscription-list'>
<span className='order-into'>{'实际支付金额'}</span>
<span className='order-price'>
{originalTotalAmount !== totalAmount &&
<span style={{fontSize: 'small', textDecoration: 'line-through', color: '#9a9a9a'}}>{'¥' + originalTotalAmount}</span>
}
<span>{'¥' + totalAmount + ' '}</span>
</span>
</dd>
</div>
</dl>
<Button className='subscription-submit' color="primary" onClick={this.onPay}>{'提交订单'}</Button>
</div>
);
};
renderAddUser = () => {
let { currentPlan, count } = this.state;
let operationIntro = '新增用户';
let originalTotalAmount = count * currentPlan.price_per_user;
let totalAmount = originalTotalAmount;
return (
<div className='d-flex flex-column subscription-container price-version-container-header subscription-add-user'>
<div className="price-version-container-top"></div>
<h3 className='user-quota-plan-name py-5'>{currentPlan.name}</h3>
<span className='py-2 mb-0 text-orange font-500 text-center'>
{'¥ '}<span className="price-version-plan-price">{currentPlan.price}</span>{' ' + currentPlan.description}
</span>
<InputGroup style={{marginBottom: '5px'}} className='user-numbers'>
<InputGroupAddon addonType="prepend">
<InputGroupText>{operationIntro}</InputGroupText>
</InputGroupAddon>
<Input
className="py-2"
placeholder={operationIntro}
title={operationIntro}
type="number"
value={count || 1}
min="1"
max="9999"
disabled={!currentPlan.can_custom_count}
onChange={this.onCountInputChange}
/>
</InputGroup>
<span className='py-2 text-orange mb-0 font-500 price-version-plan-whole-price text-center'>
{'总价 ¥ ' + totalAmount}
{originalTotalAmount !== totalAmount &&
<span style={{fontSize: 'small', textDecoration: 'line-through', color: '#9a9a9a'}}>{' ¥' + originalTotalAmount}</span>
}
</span>
<span className='py-2 mb-0 text-lg-size font-500 price-version-plan-valid-day text-center'>{'有效期至 ' + currentPlan.new_term_end}</span>
<span className='subscription-notice text-center py-5'>{'注:当有效期剩余天数少于计划中的时候,增加用户的价格按天来计算'}</span>
<Button className='subscription-submit' onClick={this.onPay} color="primary">{'立即购买'}</Button>
</div>
);
};
renderBuyQuota = () => {
let { currentPlan, assetQuotaUnitCount } = this.state;
let operationIntro = '新增空间';
let originalTotalAmount = assetQuotaUnitCount * currentPlan.price_per_asset_quota_unit;
let totalAmount = originalTotalAmount;
return (
<div className='d-flex flex-column subscription-container price-version-container-header subscription-add-space'>
<div className="price-version-container-top"></div>
<h3 className='user-quota-plan-name py-5'>{currentPlan.name}</h3>
<span className='py-2 mb-0 text-orange font-500 text-center'>
{'¥ '}<span className="price-version-plan-price">{currentPlan.asset_quota_price}</span>{' ' + currentPlan.asset_quota_description}
</span>
<InputGroup style={{marginBottom: '5px'}} className='space-quota'>
<InputGroupAddon addonType="prepend">
<InputGroupText><span className="font-500">{operationIntro}</span></InputGroupText>
</InputGroupAddon>
<Input
className="py-2"
placeholder={operationIntro}
title={operationIntro}
type="number"
value={assetQuotaUnitCount || 1}
min="1"
max="9999"
disabled={!currentPlan.can_custom_asset_quota}
onChange={this.onAssetQuotaUnitCountInputChange}
/>
<InputGroupAddon addonType='append'>
<InputGroupText><span className="font-500">{' x ' + currentPlan.asset_quota_unit + 'GB'}</span></InputGroupText>
</InputGroupAddon>
</InputGroup>
<span className='py-4 text-orange mb-0 font-500 price-version-plan-whole-price text-center'>
{'总价 ¥ ' + totalAmount}
{originalTotalAmount !== totalAmount &&
<span style={{fontSize: 'small', textDecoration: 'line-through', color: '#9a9a9a'}}>{' ¥' + originalTotalAmount}</span>
}
</span>
<span className='py-2 mb-0 text-lg-size font-500 price-version-plan-valid-day text-center'>{'有效期至 ' + currentPlan.new_term_end}</span>
<span className='subscription-notice text-center py-5'>{'注:当有效期剩余天数少于计划中的时候,增加空间的价格按天来计算'}</span>
<Button className='subscription-submit' onClick={this.onPay} color="primary">{'立即购买'}</Button>
</div>
);
};
render() {
let { paymentType } = this.props;
if (paymentType === 'paid' || paymentType === 'extend_time') {
return this.renderPaidOrExtendTime();
} else if (paymentType === 'add_user') {
return this.renderAddUser();
} else if (paymentType === 'buy_quota') {
return this.renderBuyQuota();
} else {
toaster.danger(gettext('Internal Server Error.'));
return;
}
}
}
Plans.propTypes = PlansPropTypes;
const PlansDialogPropTypes = {
isOrgContext: PropTypes.bool.isRequired,
paymentType: PropTypes.string.isRequired,
paymentTypeTrans: PropTypes.string.isRequired,
toggleDialog: PropTypes.func.isRequired,
};
class PlansDialog extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
isWaiting: false,
planList: [],
paymentSourceList: [],
};
}
getPlans = () => {
subscriptionAPI.getSubscriptionPlans(this.props.paymentType).then((res) => {
this.setState({
planList: res.data.plan_list,
paymentSourceList: res.data.payment_source_list,
isLoading: false,
});
}).catch(error => {
let errorMsg = Utils.getErrorMsg(error);
this.setState({
isLoading: false,
errorMsg: errorMsg,
});
});
};
onPay = (planID, count, asset_quota, totalAmount) => {
this.setState({ isWaiting: true });
let payUrl = serviceURL + '/subscription/pay/?payment_source=' + this.state.paymentSourceList[0] +
'&payment_type=' + this.props.paymentType + '&plan_id=' + planID +
'&total_amount=' + totalAmount;
if (count) {
payUrl += '&count=' + count;
}
if (asset_quota) {
payUrl += '&asset_quota=' + asset_quota;
}
window.open(payUrl);
};
onReload = () => {
window.location.reload();
};
componentDidMount() {
this.getPlans();
}
render() {
const { isLoading, isWaiting, planList } = this.state;
const { toggleDialog, paymentTypeTrans, paymentType } = this.props;
const modalStyle = (paymentType === 'paid' || paymentType === 'extend_time') ?
{width: '560px', maxWidth: '560px'} : {width: '560px'};
if (isLoading) {
return (
<Modal isOpen={true} toggle={toggleDialog}>
<ModalHeader toggle={toggleDialog}>{paymentTypeTrans}</ModalHeader>
<ModalBody>
<Loading />
</ModalBody>
</Modal>
);
}
if (isWaiting) {
return (
<Modal isOpen={true} toggle={this.onReload}>
<ModalHeader toggle={this.onReload}>{paymentTypeTrans}</ModalHeader>
<ModalBody>
<div>{'是否完成付款?'}</div>
</ModalBody>
<ModalFooter>
<button className="btn btn-outline-primary" onClick={this.onReload}>{'是'}</button>
</ModalFooter>
</Modal>
);
}
return (
<Modal isOpen={true} toggle={toggleDialog} style={modalStyle}>
<ModalHeader toggle={toggleDialog}>{paymentTypeTrans}</ModalHeader>
<ModalBody>
<div className="d-flex justify-content-between">
<Plans
plans={planList}
onPay={this.onPay}
paymentType={this.props.paymentType}
/>
</div>
</ModalBody>
</Modal>
);
}
}
PlansDialog.propTypes = PlansDialogPropTypes;
const propTypes = {
isOrgContext: PropTypes.bool.isRequired,
handleContentScroll: PropTypes.func,
};
class Subscription extends Component {
constructor(props) {
super(props);
this.paymentTypeTransMap = {
paid: '立即购买',
extend_time: '立即续费',
add_user: '增加用户',
buy_quota: '增加空间',
};
this.state = {
isLoading: true,
errorMsg: '',
isDialogOpen: false,
planName: this.props.isOrgContext ? '团队版' : '个人版',
userLimit: 20,
assetQuota: 1,
termEnd: '长期',
subscription: null,
paymentTypeList: [],
currentPaymentType: '',
errorMsgCode: ''
};
}
getSubscription = () => {
subscriptionAPI.getSubscription().then((res) => {
const subscription = res.data.subscription;
const paymentTypeList = res.data.payment_type_list;
if (!subscription) {
this.setState({
isLoading: false,
paymentTypeList: paymentTypeList,
});
} else {
let isActive = subscription.is_active;
let plan = subscription.plan;
this.setState({
isLoading: false,
subscription,
planName: plan.name,
userLimit: subscription.user_limit,
assetQuota: isActive ? subscription.asset_quota : plan.asset_quota,
termEnd: isActive ? subscription.term_end : '已过期',
paymentTypeList: paymentTypeList,
});
}
}).catch(error => {
let errorMsg = Utils.getErrorMsg(error);
this.setState({
isLoading: false,
errorMsg: errorMsg,
});
});
};
toggleDialog = () => {
this.setState({ isDialogOpen: !this.state.isDialogOpen });
};
togglePaymentType = (paymentType) => {
this.setState({ currentPaymentType: paymentType });
this.toggleDialog();
};
componentDidMount() {
this.getSubscription();
}
render() {
const { isLoading, errorMsg, planName, userLimit, assetQuota, termEnd,
isDialogOpen, paymentTypeList, currentPaymentType } = this.state;
if (isLoading) {
return <Loading />;
}
if (errorMsg) {
return <p className="text-center mt-8 error">{errorMsg}</p>;
}
return (
<Fragment>
<div className="content position-relative" onScroll={this.props.handleContentScroll}>
<div id="current-plan" className="subscription-info">
<h3 className="subscription-info-heading">{'当前版本'}</h3>
<p className="mb-2">{planName}</p>
</div>
{this.props.isOrgContext &&
<div id="user-limit" className="subscription-info">
<h3 className="subscription-info-heading">{'用户数限制'}</h3>
<p className="mb-2">{userLimit}</p>
</div>
}
<div id="asset-quota" className="subscription-info">
<h3 className="subscription-info-heading">{'空间'}</h3>
<p className="mb-2">{assetQuota ? assetQuota + 'GB' : '1GB'}</p>
</div>
<div id="current-subscription-period" className="subscription-info">
<h3 className="subscription-info-heading">{'订阅有效期'}</h3>
<p className="mb-2">{termEnd}</p>
</div>
<div id="product-price" className="subscription-info">
<h3 className="subscription-info-heading">{'云服务付费方案'}</h3>
<p className="mb-2">
<a rel="noopener noreferrer" target="_blank" href="https://www.seafile.com/product/private_server/">{'查看详情'}</a>
</p>
</div>
{paymentTypeList.map((item, index) => {
let name = this.paymentTypeTransMap[item];
return (
<button
key={index}
className="btn btn-outline-primary mr-4"
onClick={this.togglePaymentType.bind(this, item)}
>{name}</button>
);
})}
{!this.state.subscription &&
<div id="sales-consultant" className="subscription-info mt-6">
<h3 className="subscription-info-heading">{'销售咨询'}</h3>
<img className="mb-2" src="/media/img/qr-sale.png" alt="" width="112"></img>
<p className="mb-2">{'微信扫码联系销售'}</p>
</div>
}
</div>
{isDialogOpen &&
<PlansDialog
paymentType={currentPaymentType}
paymentTypeTrans={this.paymentTypeTransMap[currentPaymentType]}
isOrgContext={this.props.isOrgContext}
toggleDialog={this.toggleDialog}
/>
}
</Fragment>
);
}
}
Subscription.propTypes = propTypes;
export default Subscription;

View File

@ -0,0 +1,230 @@
.content {
padding: 0rem 1rem 8rem;
overflow: auto;
}
.content .dropdown .btn-outline-primary {
color: #f19645;
background-color: transparent;
background-image: none;
border-color: #f19645;
}
.content .dropdown .btn-outline-primary:hover {
color: #fff;
background-color: #f19645;
border-color: #f19645;
}
.content .dropdown .btn-outline-primary:focus {
box-shadow: 0 0 0 2px rgba(241, 150, 69, 0.5);
}
.content .dropdown .btn-outline-primary:active {
color: #fff;
background-color: #f19645;
border-color: #f19645;
}
.content .dropdown .btn-outline-primary:not(:disabled):not(.disabled):active:focus,
.content .dropdown .btn-outline-primary:not(:disabled):not(.disabled).active:focus,
.content .dropdown .show > .btn-outline-primary.dropdown-toggle:focus {
box-shadow: 0 0 0 2px rgba(241, 150, 69, 0.5);
}
.subscription-info {
margin: 1em 0 3em;
}
.subscription-info-heading {
font-size: 1rem;
font-weight: normal;
padding-bottom: 0.2em;
border-bottom: 1px solid #ddd;
margin-bottom: 0.7em;
}
.price-version-container-header {
max-width: 90%;
border-radius: 10px;
padding: 0 1rem 2rem;
box-shadow: 0 3px 8px 3px rgba(116, 129, 141, 0.15);
overflow: hidden;
margin: 0.5rem 1rem;
}
.price-version-container-header .price-version-container-top {
height: 10px;
margin: 0 -1rem;
}
.price-version-container-header .price-version-plan-name {
font-size: 1.5rem;
padding: 1rem 0;
border-bottom: 1px solid #ED7109;
}
.price-version-container-header .price-table-btn-details {
color: #fff;
font-size: 1rem;
}
.price-version-container-header .font-500 {
font-weight: 500;
}
.price-version-container-header .price-version-plan-price {
font-size: 2rem;
}
.price-version-container-header .price-version-plan-whole-price {
font-size: 24px;
}
.price-version-container-header .price-version-plan-valid-day,
.price-version-container-header .price-version-plan-user-number {
font-size: 18px;
}
.subscription-container {
width: 480px;
margin: 0.5rem auto;
}
.price-version-container-header .user-quota-plan-name {
margin-bottom: 24px;
text-align: center;
border-bottom: 1px solid rgba(253, 150, 68, 0.4);
}
.subscription-container .items-dl {
margin-top: 10px;
}
.plan-description-item {
border: 1px solid #bdbdbd;
border-radius: 5px;
margin: 0 0 10px;
display: flex;
justify-content: space-between;
background-color: #f9f9f9;
line-height: 3;
padding: 0 20px;
cursor: pointer;
}
.plan-description-item.plan-selected {
background-color: #fff5eb;
border-color: #ED7109;
border-width: 2px;
}
.plan-description-item .plan-name {
font-size: 20px;
font-weight: 500;
}
.plan-description-item .plan-description {
font-size: 20px;
font-weight: 500;
color: #ED7109;
}
.order-item {
margin: 0px;
display: flex;
justify-content: space-between;
border-left: 1px solid #bdbdbd;
border-right: 1px solid #bdbdbd;
background-color: #f9f9f9;
padding: 5px 20px;
line-height: 2;
}
.order-item.order-item-top {
border-top: 1px solid #bdbdbd;
border-radius: 5px 5px 0 0;
}
.order-item.order-item-middle {
border-bottom: 1px solid #bdbdbd;
}
.order-item.order-item-bottom {
border-bottom: 1px solid #bdbdbd;
border-radius: 0 0 5px 5px;
}
.order-item .order-into {
color: #666666;
}
.order-item .order-value {
font-weight: 500;
color: #000;
}
.order-item .order-value .input-group-append,
.order-item .order-value .input-group-prepend {
height: 38px;
}
.order-item .order-price {
font-size: 26px;
font-weight: 500;
color: #ED7109;
}
.order-item .order-price span:first-child {
font-size: 16px;
}
.subscription-submit {
background-color: #ED7109;
border-radius: 5px;
width: 100%;
margin-bottom: 10px;
color: white;
height: 44px;
font-size: 16px;
}
.subscription-notice {
font-size: 14px;
color: #9a9a9a;
}
.subscription-container .subscription-subtitle {
font-size: 18px;
font-weight: 500;
}
.subscription-container .subscription-list.order-item-top.order-item-bottom {
border-radius: 5px;
}
.subscription-container .subscription-list span {
height: 36px;
line-height: 36px;
}
.subscription-container .text-orange {
color: #ED7109 !important;
}
.user-numbers.input-group .input-group-prepend {
width: 50%;
}
.space-quota .input-group .input-group-append,
.space-quota .input-group .input-group-prepend {
width: 33.33%;
}
.subscription-add-space .input-group .input-group-append .input-group-text,
.price-version-container-header .input-group .input-group-prepend .input-group-text {
width: 100%;
text-align: center;
font-weight: 500;
display: block;
}

View File

@ -36,6 +36,7 @@ import OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update';
import OrgLogsPermAudit from './org-logs-perm-audit';
import OrgSAMLConfig from './org-saml-config';
import OrgSubscription from './org-subscription';
import '../../css/layout.css';
import '../../css/toolbar.css';
@ -96,6 +97,7 @@ class Org extends React.Component {
<OrgMobileDevices path={siteRoot + 'org/deviceadmin/mobile-devices/'} />
<OrgDevicesErrors path={siteRoot + 'org/deviceadmin/devices-errors/'} />
<OrgWebSettings path={siteRoot + 'org/web-settings'} />
<OrgSubscription path={siteRoot + 'org/subscription'} onCloseSidePanel={this.onCloseSidePanel} />
<OrgUsers path={siteRoot + 'org/useradmin'} />
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />

View File

@ -0,0 +1,32 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import MainPanelTopbar from './main-panel-topbar';
import Subscription from '../../components/subscription';
const propTypes = {
onCloseSidePanel: PropTypes.func,
};
class OrgSubscription extends Component {
render() {
return (
<Fragment>
<MainPanelTopbar onCloseSidePanel={this.props.onCloseSidePanel} />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path">
<h2 className="sf-heading">{'付费管理'}</h2>
</div>
<div className="pt-2 h-100 o-auto">
<Subscription isOrgContext={true} />
</div>
</div>
</div>
</Fragment>
);
}
}
OrgSubscription.propTypes = propTypes;
export default OrgSubscription;

View File

@ -2,7 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import Logo from '../../components/logo';
import { gettext, siteRoot, enableMultiADFS } from '../../utils/constants';
import Icon from '../../components/icon';
import { gettext, siteRoot, enableSubscription, enableMultiADFS } from '../../utils/constants';
const propTypes = {
isSidePanelClosed: PropTypes.bool.isRequired,
@ -80,6 +81,14 @@ class SidePanel extends React.Component {
<span className="nav-text">{gettext('Departments')}</span>
</Link>
</li>
{enableSubscription &&
<li className="nav-item">
<Link className={`nav-link ellipsis ${this.getActiveClass('subscription')}`} to={siteRoot + 'org/subscription/'} onClick={() => this.tabItemClick('subscription')} >
<Icon symbol='currency' />
<span className="nav-text">{'付费管理'}</span>
</Link>
</li>
}
<li className="nav-item">
<Link className={`nav-link ellipsis ${this.getActiveClass('publinkadmin')}`} to={siteRoot + 'org/publinkadmin/'} onClick={() => this.tabItemClick('publinkadmin')} >
<span className="sf2-icon-link"></span>

View File

@ -0,0 +1,72 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
import SideNav from './components/user-settings/side-nav';
import Account from './components/common/account';
import Notification from './components/common/notification';
import Subscription from './components/subscription';
import './css/toolbar.css';
import './css/search.css';
import './css/user-settings.css';
class UserSubscription extends React.Component {
constructor(props) {
super(props);
this.sideNavItems = [
{ show: true, href: '#current-plan', text: '当前版本' },
{ show: true, href: '#asset-quota', text: '空间' },
{ show: true, href: '#current-subscription-period', text: '订阅有效期' },
{ show: true, href: '#product-price', text: '云服务付费方案' },
];
this.state = {
curItemID: this.sideNavItems[0].href.substr(1),
};
}
handleContentScroll = (e) => {
// Mobile does not display the sideNav, so when scrolling don't update curItemID
const scrollTop = e.target.scrollTop;
const scrolled = this.sideNavItems.filter((item, index) => {
return item.show && document.getElementById(item.href.substr(1)).offsetTop - 45 < scrollTop;
});
if (scrolled.length) {
this.setState({
curItemID: scrolled[scrolled.length - 1].href.substr(1)
});
}
};
render() {
let logoUrl = logoPath.startsWith('http') ? logoPath : mediaUrl + logoPath;
return (
<div className="h-100 d-flex flex-column">
<div className="top-header d-flex justify-content-between">
<a href={siteRoot}>
<img src={logoUrl} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
</a>
<div className="common-toolbar">
<Notification />
<Account />
</div>
</div>
<div className="flex-auto d-flex o-hidden">
<div className="side-panel o-auto">
<SideNav data={this.sideNavItems} curItemID={this.state.curItemID} />
</div>
<div className="main-panel d-flex flex-column">
<h2 className="heading">{'付费管理'}</h2>
<Subscription isOrgContext={false} handleContentScroll={this.handleContentScroll}/>
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<UserSubscription />,
document.getElementById('wrapper')
);

View File

@ -140,6 +140,7 @@ export const orgEnableAdminCustomLogo = window.org ? window.org.pageOptions.orgE
export const orgEnableAdminCustomName = window.org ? window.org.pageOptions.orgEnableAdminCustomName === 'True' : false;
export const orgEnableAdminInviteUser = window.org ? window.org.pageOptions.orgEnableAdminInviteUser === 'True' : false;
export const enableMultiADFS = window.org ? window.org.pageOptions.enableMultiADFS === 'True' : false;
export const enableSubscription = window.org ? window.org.pageOptions.enableSubscription : false;
// sys admin
export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : '';

View File

@ -0,0 +1,61 @@
import axios from 'axios';
import cookie from 'react-cookies';
import { siteRoot } from './constants';
class SubscriptionAPI {
init({ server, username, password, token }) {
this.server = server;
this.username = username;
this.password = password;
this.token = token; //none
if (this.token && this.server) {
this.req = axios.create({
baseURL: this.server,
headers: { 'Authorization': 'Token ' + this.token },
});
}
return this;
}
initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
var server = siteRoot.substring(0, siteRoot.length - 1);
this.server = server;
} else {
this.server = siteRoot;
}
this.req = axios.create({
headers: {
'X-CSRFToken': xcsrfHeaders,
}
});
return this;
}
getSubscription() {
const url = this.server + '/api/v2.1/subscription/';
return this.req.get(url);
}
getSubscriptionPlans(paymentType) {
const url = this.server + '/api/v2.1/subscription/plans/';
let params = {
payment_type: paymentType,
};
return this.req.get(url, { params: params });
}
getSubscriptionLogs() {
const url = this.server + '/api/v2.1/subscription/logs/';
return this.req.get(url);
}
}
let subscriptionAPI = new SubscriptionAPI();
let xcsrfHeaders = cookie.load('sfcsrftoken');
subscriptionAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
export { subscriptionAPI };

View File

@ -668,6 +668,7 @@ a, a:hover { color: #ec8000; }
color: #fff;
}
.side-nav-con .seafile-multicolor-icon,
.side-nav-con [class^="sf2-icon-"],
.side-nav-con [class^="sf3-font-"],
.side-nav-con [class^="fas"] {

BIN
media/img/qr-sale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -77,6 +77,9 @@ def get_org_detailed_info(org):
users = ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)
org_info['users_count'] = len(users)
active_users_count = len([m for m in users if m.is_active])
org_info['active_users_count'] = active_users_count
repos = seafile_api.get_org_repo_list(org_id, -1, -1)
org_info['repos_count'] = len(repos)
@ -460,3 +463,30 @@ class AdminSearchOrganization(APIView):
result.append(org_info)
return Response({'organization_list': result})
class AdminOrganizationsBaseInfo(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, IsProVersion)
throttle_classes = (UserRateThrottle,)
def get(self, request):
'''
Get base info of organizations in bulk by ids
'''
if not MULTI_TENANCY:
error_msg = 'Feature is not enabled.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
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:
continue
base_info = {'org_id': org.org_id, 'org_name': org.org_name}
orgs.append(base_info)
return Response({'organization_list': orgs})

View File

@ -23,7 +23,7 @@ from seaserv import seafile_api, ccnet_api
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error, to_python_boolean
from seahub.api2.utils import api_error, to_python_boolean, get_user_common_info
from seahub.api2.models import TokenV2
from seahub.utils.ccnet_db import get_ccnet_db_name
import seahub.settings as settings
@ -2092,3 +2092,28 @@ class AdminUpdateUserCcnetEmail(APIView):
logger.error(e)
return Response({'success': True})
class AdminUserList(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, )
throttle_classes = (UserRateThrottle, )
def post(self, request):
"""return user_list by user_id_list
"""
# argument check
user_id_list = request.data.get('user_id_list')
if not isinstance(user_id_list, list):
error_msg = 'user_id_list invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# main
user_list = list()
for user_id in user_id_list:
if not isinstance(user_id, str):
continue
user_info = get_user_common_info(user_id)
user_list.append(user_info)
return Response({'user_list': user_list})

View File

@ -0,0 +1,132 @@
import logging
import requests
from rest_framework.views import APIView
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from rest_framework.response import Response
from django.utils.translation import gettext as _
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.subscription.utils import subscription_check, get_customer_id, \
get_subscription_api_headers, subscription_permission_check, \
handler_subscription_api_response
from seahub.subscription.settings import SUBSCRIPTION_SERVER_URL
logger = logging.getLogger(__name__)
class SubscriptionView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request):
"""Get subscription
"""
# check
if not subscription_check():
error_msg = _('Feature is not enabled.')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not subscription_permission_check(request):
error_msg = _('Permission denied.')
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# main
try:
customer_id = get_customer_id(request)
headers = get_subscription_api_headers()
data = {
'customer_id': customer_id,
}
url = SUBSCRIPTION_SERVER_URL.rstrip('/') + '/api/seafile/subscription/'
response = requests.get(url, params=data, headers=headers)
response = handler_subscription_api_response(response)
except Exception as e:
logger.error(e)
error_msg = _('Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(response.json(), status=response.status_code)
class SubscriptionPlansView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request):
"""Get plans
"""
# check
if not subscription_check():
error_msg = _('Feature is not enabled.')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not subscription_permission_check(request):
error_msg = _('Permission denied.')
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
payment_type = request.GET.get('payment_type')
# main
try:
customer_id = get_customer_id(request)
headers = get_subscription_api_headers()
data = {
'customer_id': customer_id,
'payment_type': payment_type,
}
url = SUBSCRIPTION_SERVER_URL.rstrip(
'/') + '/api/seafile/subscription/plans/'
response = requests.get(url, params=data, headers=headers)
response = handler_subscription_api_response(response)
except Exception as e:
logger.error(e)
error_msg = _('Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(response.json(), status=response.status_code)
class SubscriptionLogsView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request):
"""Get subscription logs by paid
"""
# check
if not subscription_check():
error_msg = _('Feature is not enabled.')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not subscription_permission_check(request):
error_msg = _('Permission denied.')
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# main
try:
customer_id = get_customer_id(request)
headers = get_subscription_api_headers()
data = {
'customer_id': customer_id,
}
url = SUBSCRIPTION_SERVER_URL.rstrip(
'/') + '/api/seafile/subscription/logs/'
response = requests.get(url, params=data, headers=headers)
response = handler_subscription_api_response(response)
except Exception as e:
logger.error(e)
error_msg = _('Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(response.json(), status=response.status_code)

View File

@ -105,7 +105,7 @@ from seahub.settings import THUMBNAIL_EXTENSION, THUMBNAIL_ROOT, \
STORAGE_CLASS_MAPPING_POLICY, \
ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, SHARE_LINK_EXPIRE_DAYS_MAX, \
SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_DEFAULT
from seahub.subscription.utils import subscription_check
try:
from seahub.settings import CLOUD_MODE
@ -331,6 +331,7 @@ class AccountInfo(APIView):
info['contact_email'] = p.contact_email if p else ""
info['institution'] = p.institution if p and p.institution else ""
info['is_staff'] = request.user.is_staff
info['enable_subscription'] = subscription_check()
if getattr(settings, 'MULTI_INSTITUTION', False):
from seahub.institutions.models import InstitutionAdmin

View File

@ -18,6 +18,8 @@
orgEnableAdminCustomName: '{{ org_enable_admin_custom_name }}',
orgEnableAdminInviteUser: '{{ org_enable_admin_invite_user }}',
enableMultiADFS: '{{ enable_multi_adfs }}',
isOrgContext: true,
enableSubscription: {% if enable_subscription %} true {% else %} false {% endif %},
}
}
</script>

View File

@ -43,4 +43,6 @@ urlpatterns = [
path('associate/<path:token>/', org_associate, name='org_associate'),
path('samlconfig/', react_fake_view, name='saml_config'),
re_path(r'^subscription/$', react_fake_view, name='org_subscription'),
]

View File

@ -35,6 +35,7 @@ from seahub.organizations.settings import ORG_AUTO_URL_PREFIX, \
ORG_ENABLE_ADMIN_CUSTOM_LOGO, ORG_ENABLE_ADMIN_CUSTOM_NAME, \
ORG_ENABLE_ADMIN_INVITE_USER
from seahub.organizations.utils import get_or_create_invitation_link
from seahub.subscription.utils import subscription_check
# Get an instance of a logger
logger = logging.getLogger(__name__)
@ -260,6 +261,7 @@ def react_fake_view(request, **kwargs):
'group_id': group_id,
'invitation_link': invitation_link,
'enable_multi_adfs': ENABLE_MULTI_ADFS,
'enable_subscription': subscription_check(),
})
@login_required

View File

@ -277,6 +277,7 @@ INSTALLED_APPS = [
'seahub.krb5_auth',
'seahub.django_cas_ng',
'seahub.seadoc',
'seahub.subscription',
]

View File

View File

@ -0,0 +1,10 @@
from django.conf import settings
ENABLE_SUBSCRIPTION = getattr(settings, 'ENABLE_SUBSCRIPTION', False)
SUBSCRIPTION_SERVER_AUTH_KEY = getattr(
settings, 'SUBSCRIPTION_SERVER_AUTH_KEY', '')
SUBSCRIPTION_SERVER_URL = getattr(
settings, 'SUBSCRIPTION_SERVER_URL', '')
SUBSCRIPTION_ORG_PREFIX = getattr(settings, 'SUBSCRIPTION_ORG_PREFIX', 'org_')

View File

@ -0,0 +1,7 @@
from django.urls import re_path
from .views import subscription_view, subscription_pay_view
urlpatterns = [
re_path(r'^$', subscription_view, name="subscription"),
re_path(r'pay/$', subscription_pay_view, name="subscription-pay"),
]

View File

@ -0,0 +1,90 @@
import logging
import requests
from django.core.cache import cache
from seahub.utils import normalize_cache_key
from seahub.utils import is_pro_version, is_org_context
from .settings import ENABLE_SUBSCRIPTION, SUBSCRIPTION_SERVER_AUTH_KEY, \
SUBSCRIPTION_SERVER_URL, SUBSCRIPTION_ORG_PREFIX
logger = logging.getLogger(__name__)
SUBSCRIPTION_TOKEN_CACHE_KEY = 'SUBSCRIPTION_TOKEN'
def subscription_check():
if not is_pro_version() or not ENABLE_SUBSCRIPTION:
return False
if not SUBSCRIPTION_SERVER_AUTH_KEY \
or not SUBSCRIPTION_SERVER_URL:
logger.error('subscription relevant settings invalid.')
logger.error(
'please check SUBSCRIPTION_SERVER_AUTH_KEY')
logger.error('SUBSCRIPTION_SERVER_URL: %s' % SUBSCRIPTION_SERVER_URL)
return False
return True
def get_subscription_jwt_token():
cache_key = normalize_cache_key(SUBSCRIPTION_TOKEN_CACHE_KEY)
jwt_token = cache.get(cache_key, None)
if not jwt_token:
data = {
'auth_key': SUBSCRIPTION_SERVER_AUTH_KEY,
}
url = SUBSCRIPTION_SERVER_URL.rstrip('/') + '/api/jwt-auth/'
response = requests.post(url, json=data)
if response.status_code >= 400:
raise ConnectionError(response.status_code, response.text)
response_dic = response.json()
jwt_token = response_dic.get('token')
cache.set(cache_key, jwt_token, 3000)
return jwt_token
def clear_subscription_jwt_token():
cache_key = normalize_cache_key(SUBSCRIPTION_TOKEN_CACHE_KEY)
cache.set(cache_key, None)
def get_subscription_api_headers():
jwt_token = get_subscription_jwt_token()
headers = {
'Authorization': 'JWT ' + jwt_token,
'Content-Type': 'application/json',
}
return headers
def handler_subscription_api_response(response):
if response.status_code == 403:
clear_subscription_jwt_token()
response.status_code = 500
return response
def subscription_permission_check(request):
if is_org_context(request):
is_org_staff = request.user.org.is_staff
if not is_org_staff:
return False
return True
def get_customer_id(request):
if is_org_context(request):
org_id = request.user.org.org_id
customer_id = SUBSCRIPTION_ORG_PREFIX + str(org_id)
else:
customer_id = request.user.username
return customer_id

View File

@ -0,0 +1,91 @@
# Copyright (c) 2012-2020 Seafile Ltd.
# encoding: utf-8
import requests
import logging
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect
from seahub.utils import render_error, is_org_context
from seahub.auth.decorators import login_required
from .utils import subscription_check, subscription_permission_check, \
get_subscription_api_headers, get_customer_id, handler_subscription_api_response
from .settings import SUBSCRIPTION_SERVER_URL
logger = logging.getLogger(__name__)
@login_required
def subscription_view(request):
"""
subscription
"""
if not subscription_check():
return render_error(request, _('Feature is not enabled.'))
if is_org_context(request):
return render_error(request, _('Permission denied.'))
return_dict = {}
template = 'subscription/subscription_react.html'
return render(request, template, return_dict)
@login_required
def subscription_pay_view(request):
"""
subscription
"""
if not subscription_check():
return render_error(request, _('Feature is not enabled.'))
if not subscription_permission_check(request):
error_msg = _('Permission denied.')
return render_error(request, error_msg)
plan_id = request.GET.get('plan_id')
payment_source = request.GET.get('payment_source')
payment_type = request.GET.get('payment_type')
count = request.GET.get('count')
asset_quota = request.GET.get('asset_quota')
total_amount = request.GET.get('total_amount')
# main
try:
customer_id = get_customer_id(request)
headers = get_subscription_api_headers()
data = {
'customer_id': customer_id,
'plan_id': plan_id,
'payment_source': payment_source,
'payment_type': payment_type,
'total_amount': total_amount,
}
if count:
data['count'] = count
if asset_quota:
data['asset_quota'] = asset_quota
url = SUBSCRIPTION_SERVER_URL.rstrip('/') + '/api/seafile/subscription/pay/'
response = requests.post(url, json=data, headers=headers)
response = handler_subscription_api_response(response)
response_dic = response.json()
if response.status_code >= 400:
error_msg = response_dic.get('error_msg')
if 'non_field_errors' in response_dic and response_dic['non_field_errors']:
error_msg = response_dic['non_field_errors'][0]
return render_error(request, error_msg)
use_redirect_url = response_dic.get('use_redirect_url')
redirect_url = response_dic.get('redirect_url')
if use_redirect_url and redirect_url:
return HttpResponseRedirect(redirect_url)
if not use_redirect_url:
return render(request, 'subscription/pay_result.html', {'info': '支付成功'})
except Exception as e:
logger.error(e)
error_msg = _('Internal Server Error')
return render_error(request, error_msg)

View File

@ -147,6 +147,7 @@
enableSeafileAI: {% if enable_seafile_ai %} true {% else %} false {% endif %},
canSetExProps: {% if can_set_ex_props %} true {% else %} false {% endif %},
enableSeaTableIntegration: {% if enable_seatable_integration %} true {% else %} false {% endif %},
isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
}
};
</script>

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block main_panel %}
<div class="text-panel">
<p>{{ info }}</p>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends 'base_for_react.html' %}
{% load seahub_tags avatar_tags i18n %}
{% load render_bundle from webpack_loader %}
{% block sub_title %}付费管理 - {% endblock %}
{% block extra_style %}
{% render_bundle 'subscription' 'css' %}
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
// overwrite the one in base_for_react.html
window.app.pageOptions = {
isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
};
</script>
{% render_bundle 'subscription' 'js' %}
{% endblock %}

View File

@ -141,7 +141,7 @@ from seahub.api2.endpoints.admin.devices import AdminDevices
from seahub.api2.endpoints.admin.device_errors import AdminDeviceErrors
from seahub.api2.endpoints.admin.users import AdminUsers, AdminUser, AdminUserResetPassword, AdminAdminUsers, \
AdminUserGroups, AdminUserShareLinks, AdminUserUploadLinks, AdminUserBeSharedRepos, \
AdminLDAPUsers, AdminSearchUser, AdminUpdateUserCcnetEmail
AdminLDAPUsers, AdminSearchUser, AdminUpdateUserCcnetEmail, AdminUserList
from seahub.api2.endpoints.admin.device_trusted_ip import AdminDeviceTrustedIP
from seahub.api2.endpoints.admin.libraries import AdminLibraries, AdminLibrary, \
AdminSearchLibrary
@ -163,7 +163,7 @@ from seahub.api2.endpoints.admin.users_batch import AdminUsersBatch, AdminAdminU
AdminImportUsers
from seahub.api2.endpoints.admin.operation_logs import AdminOperationLogs
from seahub.api2.endpoints.admin.organizations import AdminOrganizations, \
AdminOrganization, AdminSearchOrganization
AdminOrganization, AdminSearchOrganization, AdminOrganizationsBaseInfo
from seahub.api2.endpoints.admin.institutions import AdminInstitutions, AdminInstitution
from seahub.api2.endpoints.admin.institution_users import AdminInstitutionUsers, AdminInstitutionUser
from seahub.api2.endpoints.admin.org_users import AdminOrgUsers, AdminOrgUser
@ -203,6 +203,7 @@ from seahub.ocm.settings import OCM_ENDPOINT
from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskStatus, \
LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
urlpatterns = [
path('accounts/', include('seahub.base.registration_urls')),
@ -581,6 +582,7 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/ldap-users/$', AdminLDAPUsers.as_view(), name='api-v2.1-admin-ldap-users'),
re_path(r'^api/v2.1/admin/search-user/$', AdminSearchUser.as_view(), name='api-v2.1-admin-search-user'),
re_path(r'^api/v2.1/admin/update-user-ccnet-email/$', AdminUpdateUserCcnetEmail.as_view(), name='api-v2.1-admin-update-user-ccnet-email'),
re_path(r'^api/v2.1/admin/user-list/$', AdminUserList.as_view(), name='api-v2.1-admin-user-list'),
# [^...] Matches any single character not in brackets
# + Matches between one and unlimited times, as many times as possible
@ -672,6 +674,7 @@ urlpatterns = [
## admin::organizations
re_path(r'^api/v2.1/admin/organizations/$', AdminOrganizations.as_view(), name='api-v2.1-admin-organizations'),
re_path(r'^api/v2.1/admin/organizations-basic-info/$', AdminOrganizationsBaseInfo.as_view(), name='api-v2.1-admin-organizations-basic-info'),
re_path(r'^api/v2.1/admin/search-organization/$', AdminSearchOrganization.as_view(), name='api-v2.1-admin-Search-organization'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/$', AdminOrganization.as_view(), name='api-v2.1-admin-organization'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/users/$', AdminOrgUsers.as_view(), name='api-v2.1-admin-org-users'),
@ -990,3 +993,11 @@ if getattr(settings, 'CLIENT_SSO_VIA_LOCAL_BROWSER', False):
re_path(r'^client-sso/(?P<token>[^/]+)/$', client_sso, name="client_sso"),
re_path(r'^client-sso/(?P<token>[^/]+)/complete/$', client_sso_complete, name="client_sso_complete"),
]
if getattr(settings, 'ENABLE_SUBSCRIPTION', False):
urlpatterns += [
re_path(r'^subscription/', include('seahub.subscription.urls')),
re_path(r'^api/v2.1/subscription/$', SubscriptionView.as_view(), name='api-v2.1-subscription'),
re_path(r'^api/v2.1/subscription/plans/$', SubscriptionPlansView.as_view(), name='api-v2.1-subscription-plans'),
re_path(r'^api/v2.1/subscription/logs/$', SubscriptionLogsView.as_view(), name='api-v2.1-subscription-logs'),
]