Refactor the import process

- Add 'Subscribe' and 'Blacklist' modes to the importer
- Removed 'override status' and the support for the 'status' field in import files
This commit is contained in:
Kailash Nadh 2018-11-05 16:59:09 +05:30
parent 9aa413013a
commit f2c09e716c
12 changed files with 352 additions and 297 deletions

View File

@ -75,7 +75,10 @@ class App extends React.PureComponent {
} catch (e) { } catch (e) {
// If it's a GET call, throw a global notification. // If it's a GET call, throw a global notification.
if (method === cs.MethodGet) { if (method === cs.MethodGet) {
notification["error"]({ message: "Error fetching data", description: Utils.HttpError(e).message, duration: 0 }) notification["error"]({ placement: cs.MsgPosition,
message: "Error fetching data",
description: Utils.HttpError(e).message
})
} }
// Set states and show the error on the layout. // Set states and show the error on the layout.

View File

@ -321,7 +321,7 @@ class Campaigns extends React.PureComponent {
handleUpdateStatus = (record, status) => { handleUpdateStatus = (record, status) => {
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaignStatus, cs.MethodPut, { id: record.id, status: status }) this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaignStatus, cs.MethodPut, { id: record.id, status: status })
.then(() => { .then(() => {
notification["success"]({ placement: "topRight", message: `Campaign ${status}`, description: `"${record.name}" ${status}` }) notification["success"]({ placement: cs.MsgPosition, message: `Campaign ${status}`, description: `"${record.name}" ${status}` })
// Reload the table. // Reload the table.
this.fetchRecords() this.fetchRecords()
@ -333,7 +333,7 @@ class Campaigns extends React.PureComponent {
handleDeleteRecord = (record) => { handleDeleteRecord = (record) => {
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id }) this.props.modelRequest(cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id })
.then(() => { .then(() => {
notification["success"]({ placement: "topRight", message: "Campaign deleted", description: `"${record.name}" deleted` }) notification["success"]({ placement: cs.MsgPosition, message: "Campaign deleted", description: `"${record.name}" deleted` })
// Reload the table. // Reload the table.
this.fetchRecords() this.fetchRecords()
@ -349,7 +349,7 @@ class Campaigns extends React.PureComponent {
handleCloneCampaign = (record) => { handleCloneCampaign = (record) => {
this.setState({ modalWaiting: true }) this.setState({ modalWaiting: true })
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, record).then((resp) => { this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, record).then((resp) => {
notification["success"]({ placement: "topRight", notification["success"]({ placement: cs.MsgPosition,
message: "Campaign created", message: "Campaign created",
description: `${record.name} created` }) description: `${record.name} created` })

View File

@ -1,5 +1,5 @@
import React from "react" import React from "react"
import { Row, Col, Form, Select, Input, Checkbox, Upload, Button, Icon, Spin, Progress, Popconfirm, Tag, notification } from "antd" import { Row, Col, Form, Select, Input, Upload, Button, Radio, Icon, Spin, Progress, Popconfirm, Tag, notification } from "antd"
import * as cs from "./constants" import * as cs from "./constants"
const StatusNone = "none" const StatusNone = "none"
@ -11,7 +11,8 @@ const StatusFailed = "failed"
class TheFormDef extends React.PureComponent { class TheFormDef extends React.PureComponent {
state = { state = {
confirmDirty: false, confirmDirty: false,
fileList: [] fileList: [],
mode: "subscribe"
} }
componentDidMount() { componentDidMount() {
@ -32,7 +33,7 @@ class TheFormDef extends React.PureComponent {
} }
if(this.state.fileList.length < 1) { if(this.state.fileList.length < 1) {
notification["error"]({ message: "Error", description: "Select a valid file to upload" }) notification["error"]({ placement: cs.MsgPosition, message: "Error", description: "Select a valid file to upload" })
return return
} }
@ -41,11 +42,11 @@ class TheFormDef extends React.PureComponent {
params.append("file", this.state.fileList[0]) params.append("file", this.state.fileList[0])
this.props.request(cs.Routes.UploadRouteImport, cs.MethodPost, params).then(() => { this.props.request(cs.Routes.UploadRouteImport, cs.MethodPost, params).then(() => {
notification["info"]({ message: "File uploaded", notification["info"]({ placement: cs.MsgPosition, message: "File uploaded",
description: "Please wait while the import is running" }) description: "Please wait while the import is running" })
this.props.fetchimportState() this.props.fetchimportState()
}).catch(e => { }).catch(e => {
notification["error"]({ message: "Error", description: e.message }) notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
}) })
} }
@ -75,22 +76,35 @@ class TheFormDef extends React.PureComponent {
return ( return (
<Spin spinning={false}> <Spin spinning={false}>
<Form onSubmit={this.handleSubmit}> <Form onSubmit={this.handleSubmit}>
<Form.Item {...formItemLayout} label="Lists" extra="Lists to subscribe to"> <Form.Item {...formItemLayout} label="Mode">
{getFieldDecorator("lists", { rules: [{ required: true }] })( {getFieldDecorator("mode", { rules: [{ required: true }], initialValue: "subscribe" })(
<Select mode="multiple"> <Radio.Group className="mode" onChange={(e) => { this.setState({ mode: e.target.value }) }}>
{[...this.props.lists].map((v, i) => <Radio disabled={ this.props.formDisabled } value="subscribe">Subscribe</Radio>
<Select.Option value={v["id"]} key={v["id"]}>{v["name"]}</Select.Option> <Radio disabled={ this.props.formDisabled } value="blacklist">Blacklist</Radio>
</Radio.Group>
)}
</Form.Item>
{ this.state.mode === "subscribe" &&
<React.Fragment>
<Form.Item {...formItemLayout} label="Lists" extra="Lists to subscribe to">
{getFieldDecorator("lists", { rules: [{ required: true }] })(
<Select mode="multiple">
{[...this.props.lists].map((v, i) =>
<Select.Option value={v["id"]} key={v["id"]}>{v["name"]}</Select.Option>
)}
</Select>
)} )}
</Select> </Form.Item>
)} </React.Fragment>
</Form.Item> }
<Form.Item {...formItemLayout} { this.state.mode === "blacklist" &&
label="Override status?" <Form.Item {...formItemTailLayout}>
extra="For existing subscribers in the system found again in the import, override their status, for example 'blacklisted' to 'active'. This is not always desirable."> <p className="ant-form-extra">
{getFieldDecorator("override_status", )( All existing subscribers found in the import will be marked as 'blacklisted' and will be
<Checkbox initialValue="1" /> unsubscribed from their existing subscriptions. New subscribers will be imported and marked as 'blacklisted'.
)} </p>
</Form.Item> </Form.Item>
}
<Form.Item {...formItemLayout} label="CSV column delimiter" extra="Default delimiter is comma"> <Form.Item {...formItemLayout} label="CSV column delimiter" extra="Default delimiter is comma">
{getFieldDecorator("delim", { {getFieldDecorator("delim", {
initialValue: "," initialValue: ","
@ -113,14 +127,14 @@ class TheFormDef extends React.PureComponent {
<p className="ant-upload-drag-icon"> <p className="ant-upload-drag-icon">
<Icon type="inbox" /> <Icon type="inbox" />
</p> </p>
<p className="ant-upload-text">Click or drag file here</p> <p className="ant-upload-text">Click or drag the ZIP file here</p>
</Upload.Dragger> </Upload.Dragger>
)} )}
</div> </div>
</Form.Item> </Form.Item>
<Form.Item {...formItemTailLayout}> <Form.Item {...formItemTailLayout}>
<p className="text-grey">For existing subscribers, the names and attributes will be overwritten with the values in the CSV.</p> <p className="ant-form-extra">For existing subscribers, the names and attributes will be overwritten with the values in the CSV.</p>
<Button type="primary" htmlType="submit"><Icon type="upload" /> Upload &amp; import</Button> <Button type="primary" htmlType="submit"><Icon type="upload" /> Upload</Button>
</Form.Item> </Form.Item>
</Form> </Form>
</Spin> </Spin>
@ -140,7 +154,7 @@ class Importing extends React.PureComponent {
this.props.request(cs.Routes.UploadRouteImport, cs.MethodDelete).then((r) => { this.props.request(cs.Routes.UploadRouteImport, cs.MethodDelete).then((r) => {
this.props.fetchimportState() this.props.fetchimportState()
}).catch(e => { }).catch(e => {
notification["error"]({ message: "Error", description: e.message }) notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
}) })
} }
@ -167,7 +181,7 @@ class Importing extends React.PureComponent {
let t = document.querySelector("#log-textarea") let t = document.querySelector("#log-textarea")
t.scrollTop = t.scrollHeight; t.scrollTop = t.scrollHeight;
}).catch(e => { }).catch(e => {
notification["error"]({ message: "Error", description: e.message }) notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
}) })
} }
@ -257,7 +271,7 @@ class Import extends React.PureComponent {
this.props.request(cs.Routes.GetRouteImportStats, cs.MethodGet).then((r) => { this.props.request(cs.Routes.GetRouteImportStats, cs.MethodGet).then((r) => {
this.setState({ importState: r.data.data }) this.setState({ importState: r.data.data })
}).catch(e => { }).catch(e => {
notification["error"]({ message: "Error", description: e.message }) notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
}) })
} }

View File

@ -69,6 +69,9 @@ body {
display: none; display: none;
} }
/* Form */
/* Table actions */ /* Table actions */
td .actions a { td .actions a {
display: inline-block; display: inline-block;

View File

@ -2,6 +2,17 @@
# yarn lockfile v1 # yarn lockfile v1
"@ant-design/icons-react@~1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@ant-design/icons-react/-/icons-react-1.1.2.tgz#df25c4560864f8a3b687b305c3238daff048ed72"
dependencies:
ant-design-palettes "^1.1.3"
babel-runtime "^6.26.0"
"@ant-design/icons@~1.1.15":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-1.1.15.tgz#2ff689b87bb160c246a07adaa99cdb1c8dfd4412"
"@babel/helper-module-imports@^7.0.0": "@babel/helper-module-imports@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
@ -91,9 +102,9 @@ acorn@^5.0.0, acorn@^5.5.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
add-dom-event-listener@1.x: add-dom-event-listener@^1.1.0:
version "1.0.2" version "1.1.0"
resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz#8faed2c41008721cf111da1d30d995b85be42bed" resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310"
dependencies: dependencies:
object-assign "4.x" object-assign "4.x"
@ -199,58 +210,66 @@ ansistyles@~0.1.3:
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
antd@^3.6.5: ant-design-palettes@^1.1.3:
version "3.8.1" version "1.1.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-3.8.1.tgz#1780acd5e9bc6c80dc23b042161418eb88f4d80e" resolved "https://registry.yarnpkg.com/ant-design-palettes/-/ant-design-palettes-1.1.3.tgz#84119b1a4d86363adc52a38d587e65336a0a27dd"
dependencies: dependencies:
array-tree-filter "^2.0.0" tinycolor2 "^1.4.1"
antd@^3.6.5:
version "3.10.4"
resolved "https://registry.yarnpkg.com/antd/-/antd-3.10.4.tgz#e975c14e726801c7203d879a866f5ce8e47328c8"
dependencies:
"@ant-design/icons" "~1.1.15"
"@ant-design/icons-react" "~1.1.2"
array-tree-filter "^2.1.0"
babel-runtime "6.x" babel-runtime "6.x"
classnames "~2.2.0" classnames "~2.2.6"
create-react-class "^15.6.0" create-react-class "^15.6.3"
create-react-context "^0.2.2" create-react-context "0.2.3"
css-animation "^1.2.5" css-animation "^1.4.1"
dom-closest "^0.2.0" dom-closest "^0.2.0"
enquire.js "^2.1.1" enquire.js "^2.1.6"
intersperse "^1.0.0" intersperse "^1.0.0"
lodash "^4.17.5" lodash "^4.17.11"
moment "^2.19.3" moment "^2.22.2"
omit.js "^1.0.0" omit.js "^1.0.0"
prop-types "^15.5.7" prop-types "^15.6.2"
raf "^3.4.0" raf "^3.4.0"
rc-animate "^2.4.1" rc-animate "^2.5.4"
rc-calendar "~9.6.0" rc-calendar "~9.7.9"
rc-cascader "~0.14.0" rc-cascader "~0.16.0"
rc-checkbox "~2.1.5" rc-checkbox "~2.1.5"
rc-collapse "~1.9.0" rc-collapse "~1.10.0"
rc-dialog "~7.2.0" rc-dialog "~7.2.1"
rc-drawer "~1.6.2" rc-drawer "~1.7.6"
rc-dropdown "~2.2.0" rc-dropdown "~2.2.1"
rc-editor-mention "^1.0.2" rc-editor-mention "^1.1.7"
rc-form "^2.1.0" rc-form "^2.2.3"
rc-input-number "~4.0.0" rc-input-number "~4.3.0"
rc-menu "~7.0.2" rc-menu "~7.4.12"
rc-notification "~3.2.0" rc-notification "~3.2.0"
rc-pagination "~1.16.1" rc-pagination "~1.17.3"
rc-progress "~2.2.2" rc-progress "~2.2.6"
rc-rate "~2.4.0" rc-rate "~2.4.2"
rc-select "~8.1.1" rc-select "~8.4.0"
rc-slider "~8.6.0" rc-slider "~8.6.3"
rc-steps "~3.1.0" rc-steps "~3.3.0"
rc-switch "~1.6.0" rc-switch "~1.8.0"
rc-table "~6.2.0" rc-table "~6.3.4"
rc-tabs "~9.3.3" rc-tabs "~9.4.6"
rc-time-picker "~3.3.0" rc-time-picker "~3.4.0"
rc-tooltip "~3.7.0" rc-tooltip "~3.7.3"
rc-tree "~1.13.0" rc-tree "~1.14.6"
rc-tree-select "~2.0.5" rc-tree-select "~2.3.1"
rc-trigger "^2.5.4" rc-trigger "^2.6.2"
rc-upload "~2.5.0" rc-upload "~2.6.0"
rc-util "^4.0.4" rc-util "^4.5.1"
react-lazy-load "^3.0.12" react-lazy-load "^3.0.13"
react-lifecycles-compat "^3.0.2" react-lifecycles-compat "^3.0.4"
react-slick "~0.23.1" react-slick "~0.23.2"
shallowequal "^1.0.1" shallowequal "^1.1.0"
warning "~4.0.1" warning "~4.0.2"
anymatch@^1.3.0: anymatch@^1.3.0:
version "1.3.2" version "1.3.2"
@ -361,7 +380,7 @@ array-tree-filter@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-1.0.1.tgz#0a8ad1eefd38ce88858632f9cc0423d7634e4d5d" resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-1.0.1.tgz#0a8ad1eefd38ce88858632f9cc0423d7634e4d5d"
array-tree-filter@^2.0.0: array-tree-filter@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz#873ac00fec83749f255ac8dd083814b4f6329190" resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz#873ac00fec83749f255ac8dd083814b4f6329190"
@ -425,7 +444,7 @@ async-each@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
async-validator@1.x: async-validator@~1.8.5:
version "1.8.5" version "1.8.5"
resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-1.8.5.tgz#dc3e08ec1fd0dddb67e60842f02c0cd1cec6d7f0" resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-1.8.5.tgz#dc3e08ec1fd0dddb67e60842f02c0cd1cec6d7f0"
dependencies: dependencies:
@ -1672,7 +1691,7 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@2.x, classnames@^2.2.0, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@~2.2.0: classnames@2.x, classnames@^2.2.0, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@~2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
@ -1973,7 +1992,7 @@ copy-descriptor@^0.1.0:
core-js@^1.0.0: core-js@^1.0.0:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" resolved "http://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
core-js@^2.4.0, core-js@^2.5.0: core-js@^2.4.0, core-js@^2.5.0:
version "2.5.7" version "2.5.7"
@ -2029,7 +2048,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
create-react-class@^15.5.2, create-react-class@^15.5.3, create-react-class@^15.6.0: create-react-class@^15.5.2, create-react-class@^15.5.3, create-react-class@^15.6.0, create-react-class@^15.6.3:
version "15.6.3" version "15.6.3"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
dependencies: dependencies:
@ -2037,9 +2056,9 @@ create-react-class@^15.5.2, create-react-class@^15.5.3, create-react-class@^15.6
loose-envify "^1.3.1" loose-envify "^1.3.1"
object-assign "^4.1.1" object-assign "^4.1.1"
create-react-context@^0.2.2: create-react-context@0.2.3:
version "0.2.2" version "0.2.3"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca" resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
dependencies: dependencies:
fbjs "^0.8.0" fbjs "^0.8.0"
gud "^1.0.0" gud "^1.0.0"
@ -2072,7 +2091,7 @@ crypto-random-string@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
css-animation@1.x, css-animation@^1.2.5, css-animation@^1.3.2: css-animation@1.x, css-animation@^1.3.2, css-animation@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.4.1.tgz#5b8813125de0fbbbb0bbe1b472ae84221469b7a8" resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.4.1.tgz#5b8813125de0fbbbb0bbe1b472ae84221469b7a8"
dependencies: dependencies:
@ -2599,7 +2618,7 @@ enhanced-resolve@^3.4.0:
object-assign "^4.0.1" object-assign "^4.0.1"
tapable "^0.2.7" tapable "^0.2.7"
enquire.js@^2.1.1, enquire.js@^2.1.6: enquire.js@^2.1.6:
version "2.1.6" version "2.1.6"
resolved "https://registry.yarnpkg.com/enquire.js/-/enquire.js-2.1.6.tgz#3e8780c9b8b835084c3f60e166dbc3c2a3c89814" resolved "https://registry.yarnpkg.com/enquire.js/-/enquire.js-2.1.6.tgz#3e8780c9b8b835084c3f60e166dbc3c2a3c89814"
@ -3865,12 +3884,18 @@ iconv-lite@0.4.19:
version "0.4.19" version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: iconv-lite@^0.4.17, iconv-lite@^0.4.4:
version "0.4.23" version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
dependencies:
safer-buffer ">= 2.1.2 < 3"
icss-replace-symbols@^1.1.0: icss-replace-symbols@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@ -3913,7 +3938,7 @@ immutable@^3.7.4:
immutable@~3.7.4: immutable@~3.7.4:
version "3.7.6" version "3.7.6"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" resolved "http://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
import-lazy@^2.1.0: import-lazy@^2.1.0:
version "2.1.0" version "2.1.0"
@ -5058,10 +5083,14 @@ lodash.without@~4.4.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
"lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.16.5, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: "lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.3.0:
version "4.17.10" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
lodash@^4.16.5, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
loglevel@^1.4.1: loglevel@^1.4.1:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
@ -5304,9 +5333,9 @@ mimic-fn@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
mini-store@^1.0.2, mini-store@^1.1.0: mini-store@^2.0.0:
version "1.1.2" version "2.0.0"
resolved "https://registry.yarnpkg.com/mini-store/-/mini-store-1.1.2.tgz#cc150e0878e080ca58219d47fccefefe2c9aea3e" resolved "https://registry.yarnpkg.com/mini-store/-/mini-store-2.0.0.tgz#0843c048d6942ce55e3e78b1b67fc063022b5488"
dependencies: dependencies:
hoist-non-react-statics "^2.3.1" hoist-non-react-statics "^2.3.1"
prop-types "^15.6.0" prop-types "^15.6.0"
@ -5401,7 +5430,7 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi
dependencies: dependencies:
minimist "0.0.8" minimist "0.0.8"
moment@2.x, moment@^2.19.3: moment@2.x, moment@^2.22.2:
version "2.22.2" version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
@ -5435,6 +5464,10 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1" dns-packet "^1.3.1"
thunky "^1.0.2" thunky "^1.0.2"
mutationobserver-shim@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#f4d5dae7a4971a2207914fb5a90ebd514b65acca"
mute-stream@0.0.7, mute-stream@~0.0.4: mute-stream@0.0.7, mute-stream@~0.0.4:
version "0.0.7" version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
@ -6580,6 +6613,10 @@ preserve@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@^1.14.3:
version "1.14.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895"
pretty-bytes@^4.0.2: pretty-bytes@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
@ -6649,7 +6686,7 @@ promzard@^0.3.0:
dependencies: dependencies:
read "1" read "1"
prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1: prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies: dependencies:
@ -6794,12 +6831,18 @@ qw@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
raf@3.4.0, raf@^3.4.0: raf@3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
dependencies: dependencies:
performance-now "^2.1.0" performance-now "^2.1.0"
raf@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
dependencies:
performance-now "^2.1.0"
randomatic@^3.0.0: randomatic@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923"
@ -6843,17 +6886,20 @@ rc-align@^2.4.0, rc-align@^2.4.1:
prop-types "^15.5.8" prop-types "^15.5.8"
rc-util "^4.0.4" rc-util "^4.0.4"
rc-animate@2.x, rc-animate@^2.3.0, rc-animate@^2.4.1: rc-animate@2.x, rc-animate@^2.3.0, rc-animate@^2.5.4:
version "2.4.4" version "2.5.4"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.4.4.tgz#a05a784c747beef140d99ff52b6117711bef4b1e" resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.5.4.tgz#3b308c42e137a2e3fb578650fdb145c2100fcc35"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.6"
css-animation "^1.3.2" css-animation "^1.3.2"
prop-types "15.x" prop-types "15.x"
raf "^3.4.0"
react-lifecycles-compat "^3.0.4"
rc-animate@3.0.0-rc.1: rc-animate@^3.0.0-rc.1, rc-animate@^3.0.0-rc.4, rc-animate@^3.0.0-rc.5:
version "3.0.0-rc.1" version "3.0.0-rc.6"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.0.0-rc.1.tgz#39703e5be6d35c0c50ae53126a6c2424e5ec9405" resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.0.0-rc.6.tgz#04288eefa118e0cae214536c8a903ffaac1bc3fb"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.5" classnames "^2.2.5"
@ -6864,22 +6910,9 @@ rc-animate@3.0.0-rc.1:
rc-util "^4.5.0" rc-util "^4.5.0"
react-lifecycles-compat "^3.0.4" react-lifecycles-compat "^3.0.4"
rc-animate@^3.0.0-rc.1, rc-animate@^3.0.0-rc.4: rc-calendar@~9.7.9:
version "3.0.0-rc.4" version "9.7.11"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.0.0-rc.4.tgz#caddbd849f01d987965c6237afe64167679497bd" resolved "https://registry.yarnpkg.com/rc-calendar/-/rc-calendar-9.7.11.tgz#fa27a6e47018eb71eb6d1857cdcbd161c266dbe0"
dependencies:
babel-runtime "6.x"
classnames "^2.2.5"
component-classes "^1.2.6"
fbjs "^0.8.16"
prop-types "15.x"
raf "^3.4.0"
rc-util "^4.5.0"
react-lifecycles-compat "^3.0.4"
rc-calendar@~9.6.0:
version "9.6.2"
resolved "https://registry.yarnpkg.com/rc-calendar/-/rc-calendar-9.6.2.tgz#c7309db41225f4b8c81d5a1dcbe46d8ce07b6aee"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "2.x" classnames "2.x"
@ -6889,9 +6922,9 @@ rc-calendar@~9.6.0:
rc-trigger "^2.2.0" rc-trigger "^2.2.0"
rc-util "^4.1.1" rc-util "^4.1.1"
rc-cascader@~0.14.0: rc-cascader@~0.16.0:
version "0.14.0" version "0.16.0"
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-0.14.0.tgz#a956c99896f10883bf63d46fb894d0cb326842a4" resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-0.16.0.tgz#11baa854c2aaa2d6a8f601dec75dd136d59c5156"
dependencies: dependencies:
array-tree-filter "^1.0.0" array-tree-filter "^1.0.0"
prop-types "^15.5.8" prop-types "^15.5.8"
@ -6909,35 +6942,35 @@ rc-checkbox@~2.1.5:
prop-types "15.x" prop-types "15.x"
rc-util "^4.0.4" rc-util "^4.0.4"
rc-collapse@~1.9.0: rc-collapse@~1.10.0:
version "1.9.3" version "1.10.0"
resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-1.9.3.tgz#d9741db06a823353e1fd1aec3ba4c0f9d8af4b26" resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-1.10.0.tgz#b39578633a1e033391597776758763a10d3bc261"
dependencies: dependencies:
classnames "2.x" classnames "2.x"
css-animation "1.x" css-animation "1.x"
prop-types "^15.5.6" prop-types "^15.5.6"
rc-animate "2.x" rc-animate "2.x"
rc-dialog@~7.2.0: rc-dialog@~7.2.1:
version "7.2.0" version "7.2.1"
resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-7.2.0.tgz#b56a2d3a56003437d6ff8917eac6b0fc601936ab" resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-7.2.1.tgz#ac92fcffdf2a0eaa64b77f829336653d911a57be"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
rc-animate "2.x" rc-animate "2.x"
rc-util "^4.4.0" rc-util "^4.4.0"
rc-drawer@~1.6.2: rc-drawer@~1.7.6:
version "1.6.3" version "1.7.6"
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-1.6.3.tgz#f866b7fbde2d307b59cfd06c015ae697017db388" resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-1.7.6.tgz#925ce0768cf81ef5fa83eb22d90c603422b1b1b1"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.5" classnames "^2.2.5"
prop-types "^15.5.0" prop-types "^15.5.0"
rc-util "^4.5.1" rc-util "^4.5.1"
rc-dropdown@~2.2.0: rc-dropdown@~2.2.1:
version "2.2.0" version "2.2.1"
resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-2.2.0.tgz#a905067666ce73c0ce26cf0980e7b75b46126825" resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-2.2.1.tgz#172b6e87f0909fe8ab983e375f62e2866f3250c3"
dependencies: dependencies:
babel-runtime "^6.26.0" babel-runtime "^6.26.0"
prop-types "^15.5.8" prop-types "^15.5.8"
@ -6945,8 +6978,8 @@ rc-dropdown@~2.2.0:
react-lifecycles-compat "^3.0.2" react-lifecycles-compat "^3.0.2"
rc-editor-core@~0.8.3: rc-editor-core@~0.8.3:
version "0.8.6" version "0.8.8"
resolved "https://registry.yarnpkg.com/rc-editor-core/-/rc-editor-core-0.8.6.tgz#e48b288286effb3272cbc9c6f801450dcdb0b247" resolved "https://registry.yarnpkg.com/rc-editor-core/-/rc-editor-core-0.8.8.tgz#331034cb8d50df218839fb399cdfb2a913e71630"
dependencies: dependencies:
babel-runtime "^6.26.0" babel-runtime "^6.26.0"
classnames "^2.2.5" classnames "^2.2.5"
@ -6956,23 +6989,24 @@ rc-editor-core@~0.8.3:
prop-types "^15.5.8" prop-types "^15.5.8"
setimmediate "^1.0.5" setimmediate "^1.0.5"
rc-editor-mention@^1.0.2: rc-editor-mention@^1.1.7:
version "1.1.7" version "1.1.8"
resolved "https://registry.yarnpkg.com/rc-editor-mention/-/rc-editor-mention-1.1.7.tgz#c72d181859beda96669f4b43e19a941e68fa985b" resolved "https://registry.yarnpkg.com/rc-editor-mention/-/rc-editor-mention-1.1.8.tgz#08dafc50b1ab9ad4d2355eedd6308373ea66473a"
dependencies: dependencies:
babel-runtime "^6.23.0" babel-runtime "^6.23.0"
classnames "^2.2.5" classnames "^2.2.5"
dom-scroll-into-view "^1.2.0" dom-scroll-into-view "^1.2.0"
draft-js "~0.10.0" draft-js "~0.10.0"
immutable "^3.7.4"
prop-types "^15.5.8" prop-types "^15.5.8"
rc-animate "^2.3.0" rc-animate "^2.3.0"
rc-editor-core "~0.8.3" rc-editor-core "~0.8.3"
rc-form@^2.1.0: rc-form@^2.2.3:
version "2.2.1" version "2.2.6"
resolved "https://registry.yarnpkg.com/rc-form/-/rc-form-2.2.1.tgz#9c6d968f1460fd3872d2e9371324d717775af260" resolved "https://registry.yarnpkg.com/rc-form/-/rc-form-2.2.6.tgz#737bd1eb1f45c6ce821854e86248e145adb6230c"
dependencies: dependencies:
async-validator "1.x" async-validator "~1.8.5"
babel-runtime "6.x" babel-runtime "6.x"
create-react-class "^15.5.3" create-react-class "^15.5.3"
dom-scroll-into-view "1.x" dom-scroll-into-view "1.x"
@ -6988,9 +7022,9 @@ rc-hammerjs@~0.6.0:
hammerjs "^2.0.8" hammerjs "^2.0.8"
prop-types "^15.5.9" prop-types "^15.5.9"
rc-input-number@~4.0.0: rc-input-number@~4.3.0:
version "4.0.12" version "4.3.1"
resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-4.0.12.tgz#99b62f0b2395e1e76c9f72142b4ad7ea46f97d06" resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-4.3.1.tgz#f4b7f414f00816fd803ce5f11aac25bd38dd0f22"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.0" classnames "^2.2.0"
@ -6999,31 +7033,20 @@ rc-input-number@~4.0.0:
rc-util "^4.5.1" rc-util "^4.5.1"
rmc-feedback "^2.0.0" rmc-feedback "^2.0.0"
rc-menu@^7.0.2: rc-menu@^7.3.0, rc-menu@~7.4.12:
version "7.2.6" version "7.4.19"
resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-7.2.6.tgz#b18b8da9d637533145c6bc28cae1f29ae55c8dc7" resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-7.4.19.tgz#b65a35215966053fe6f6c3b7b12155051969f567"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "2.x" classnames "2.x"
dom-scroll-into-view "1.x" dom-scroll-into-view "1.x"
mini-store "^1.1.0" mini-store "^2.0.0"
prop-types "^15.5.6" mutationobserver-shim "^0.3.2"
rc-animate "2.x"
rc-trigger "^2.3.0"
rc-util "^4.1.0"
rc-menu@~7.0.2:
version "7.0.5"
resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-7.0.5.tgz#986b65df5ad227aadf399ea374b98d2313802316"
dependencies:
babel-runtime "6.x"
classnames "2.x"
dom-scroll-into-view "1.x"
mini-store "^1.1.0"
prop-types "^15.5.6" prop-types "^15.5.6"
rc-animate "2.x" rc-animate "2.x"
rc-trigger "^2.3.0" rc-trigger "^2.3.0"
rc-util "^4.1.0" rc-util "^4.1.0"
resize-observer-polyfill "^1.5.0"
rc-notification@~3.2.0: rc-notification@~3.2.0:
version "3.2.0" version "3.2.0"
@ -7035,21 +7058,21 @@ rc-notification@~3.2.0:
rc-animate "2.x" rc-animate "2.x"
rc-util "^4.0.4" rc-util "^4.0.4"
rc-pagination@~1.16.1: rc-pagination@~1.17.3:
version "1.16.5" version "1.17.3"
resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-1.16.5.tgz#550a758035e1957ccfa2f71ee6e55657da729679" resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-1.17.3.tgz#4c5334adef607f7a80b90ca7501a1e962491aae5"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
prop-types "^15.5.7" prop-types "^15.5.7"
rc-progress@~2.2.2: rc-progress@~2.2.6:
version "2.2.5" version "2.2.6"
resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-2.2.5.tgz#e61d0544bf9d4208e5ba32fc50962159e7f952a3" resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-2.2.6.tgz#d5d07c07333b352a9ef13230c5940e13336c1e62"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
prop-types "^15.5.8" prop-types "^15.5.8"
rc-rate@~2.4.0: rc-rate@~2.4.2:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/rc-rate/-/rc-rate-2.4.2.tgz#c097bfdba7a5783cec287c928b1461cc1621f836" resolved "https://registry.yarnpkg.com/rc-rate/-/rc-rate-2.4.2.tgz#c097bfdba7a5783cec287c928b1461cc1621f836"
dependencies: dependencies:
@ -7058,9 +7081,9 @@ rc-rate@~2.4.0:
prop-types "^15.5.8" prop-types "^15.5.8"
rc-util "^4.3.0" rc-util "^4.3.0"
rc-select@~8.1.1: rc-select@~8.4.0:
version "8.1.1" version "8.4.4"
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-8.1.1.tgz#89335cf0af518affe201bb6beefe161e1b4b2ab6" resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-8.4.4.tgz#b293bee3ebc2a45d4886b2b5f80b06f6d5a02b77"
dependencies: dependencies:
babel-runtime "^6.23.0" babel-runtime "^6.23.0"
classnames "2.x" classnames "2.x"
@ -7069,15 +7092,15 @@ rc-select@~8.1.1:
prop-types "^15.5.8" prop-types "^15.5.8"
raf "^3.4.0" raf "^3.4.0"
rc-animate "2.x" rc-animate "2.x"
rc-menu "^7.0.2" rc-menu "^7.3.0"
rc-trigger "^2.2.0" rc-trigger "^2.5.4"
rc-util "^4.0.4" rc-util "^4.0.4"
react-lifecycles-compat "^3.0.2" react-lifecycles-compat "^3.0.2"
warning "^3.0.0" warning "^4.0.2"
rc-slider@~8.6.0: rc-slider@~8.6.3:
version "8.6.1" version "8.6.3"
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.6.1.tgz#ee5e0380dbdf4b5de6955a265b0d4ff6196405d1" resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.6.3.tgz#1ca0e0bd2863252741de75e7bf8c9f2cfcffccb7"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.5" classnames "^2.2.5"
@ -7087,41 +7110,41 @@ rc-slider@~8.6.0:
shallowequal "^1.0.1" shallowequal "^1.0.1"
warning "^3.0.0" warning "^3.0.0"
rc-steps@~3.1.0: rc-steps@~3.3.0:
version "3.1.1" version "3.3.0"
resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-3.1.1.tgz#79583ad808309d82b8e011676321d153fd7ca403" resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-3.3.0.tgz#8817c438a6a5648997c7edb51bde727e6f32e132"
dependencies: dependencies:
babel-runtime "^6.23.0" babel-runtime "^6.23.0"
classnames "^2.2.3" classnames "^2.2.3"
lodash "^4.17.5" lodash "^4.17.5"
prop-types "^15.5.7" prop-types "^15.5.7"
rc-switch@~1.6.0: rc-switch@~1.8.0:
version "1.6.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/rc-switch/-/rc-switch-1.6.0.tgz#c2d7369bdb87c1fd45e84989a27c1fb2f201d2fd" resolved "https://registry.yarnpkg.com/rc-switch/-/rc-switch-1.8.0.tgz#cff32fd04c406d8c0c0397e69bc36350a333e236"
dependencies: dependencies:
babel-runtime "^6.23.0" babel-runtime "^6.23.0"
classnames "^2.2.1" classnames "^2.2.1"
prop-types "^15.5.6" prop-types "^15.5.6"
rc-table@~6.2.0: rc-table@~6.3.4:
version "6.2.8" version "6.3.7"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-6.2.8.tgz#c06bf48bfab60754ab3f70102290e41e8b32388e" resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-6.3.7.tgz#0de4f71415abe3a8d9f9937ab7ddd142ab6143b7"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.5" classnames "^2.2.5"
component-classes "^1.2.6" component-classes "^1.2.6"
lodash "^4.17.5" lodash "^4.17.5"
mini-store "^1.0.2" mini-store "^2.0.0"
prop-types "^15.5.8" prop-types "^15.5.8"
rc-util "^4.0.4" rc-util "^4.0.4"
react-lifecycles-compat "^3.0.2" react-lifecycles-compat "^3.0.2"
shallowequal "^1.0.2" shallowequal "^1.0.2"
warning "^3.0.0" warning "^3.0.0"
rc-tabs@~9.3.3: rc-tabs@~9.4.6:
version "9.3.6" version "9.4.8"
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-9.3.6.tgz#873890b3a68164a5814f89e343270b1ce9eb6acd" resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-9.4.8.tgz#62144284189d9e36472b6a6f6948bdba715bbccc"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "2.x" classnames "2.x"
@ -7131,9 +7154,9 @@ rc-tabs@~9.3.3:
rc-util "^4.0.4" rc-util "^4.0.4"
warning "^3.0.0" warning "^3.0.0"
rc-time-picker@~3.3.0: rc-time-picker@~3.4.0:
version "3.3.1" version "3.4.0"
resolved "https://registry.yarnpkg.com/rc-time-picker/-/rc-time-picker-3.3.1.tgz#94f8bbd51e6b93de1f01e78064aef1e6d765b367" resolved "https://registry.yarnpkg.com/rc-time-picker/-/rc-time-picker-3.4.0.tgz#274e80122f885b37a4eace7393f3a25334fa141f"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "2.x" classnames "2.x"
@ -7141,56 +7164,45 @@ rc-time-picker@~3.3.0:
prop-types "^15.5.8" prop-types "^15.5.8"
rc-trigger "^2.2.0" rc-trigger "^2.2.0"
rc-tooltip@^3.7.0, rc-tooltip@~3.7.0: rc-tooltip@^3.7.0, rc-tooltip@~3.7.3:
version "3.7.2" version "3.7.3"
resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.2.tgz#3698656d4bacd51b72d9e327bed15d1d5a9f1b27" resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.3.tgz#280aec6afcaa44e8dff0480fbaff9e87fc00aecc"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
prop-types "^15.5.8" prop-types "^15.5.8"
rc-trigger "^2.2.2" rc-trigger "^2.2.2"
rc-tree-select@~2.0.5: rc-tree-select@~2.3.1:
version "2.0.13" version "2.3.2"
resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-2.0.13.tgz#7bd1d5de61cb69d8048bf0d29dc3785aee6815db" resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-2.3.2.tgz#bf617a55c0ed365feeab5c3ee5ff5cf4dd5c7fc2"
dependencies: dependencies:
babel-runtime "^6.23.0" babel-runtime "^6.23.0"
classnames "^2.2.1" classnames "^2.2.1"
prop-types "^15.5.8" prop-types "^15.5.8"
raf "^3.4.0" raf "^3.4.0"
rc-animate "^3.0.0-rc.4" rc-animate "^3.0.0-rc.4"
rc-tree "~1.12.2" rc-tree "~1.14.3"
rc-trigger "^3.0.0-rc.2" rc-trigger "^3.0.0-rc.2"
rc-util "^4.5.0" rc-util "^4.5.0"
react-lifecycles-compat "^3.0.4" react-lifecycles-compat "^3.0.4"
shallowequal "^1.0.2" shallowequal "^1.0.2"
warning "^4.0.1" warning "^4.0.1"
rc-tree@~1.12.2: rc-tree@~1.14.3, rc-tree@~1.14.6:
version "1.12.7" version "1.14.8"
resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-1.12.7.tgz#94ce4b59d27325c555d6238c7b92feeaa5d476a0" resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-1.14.8.tgz#31a9652d71c015370d7b6c2109c865244deb9fde"
dependencies: dependencies:
babel-runtime "^6.23.0" babel-runtime "^6.23.0"
classnames "2.x" classnames "2.x"
prop-types "^15.5.8" prop-types "^15.5.8"
rc-animate "2.x" rc-animate "^3.0.0-rc.5"
rc-util "^4.0.4"
warning "^3.0.0"
rc-tree@~1.13.0:
version "1.13.2"
resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-1.13.2.tgz#47cd31a51871f26f50e34167d9dcf36a68c44955"
dependencies:
babel-runtime "^6.23.0"
classnames "2.x"
prop-types "^15.5.8"
rc-animate "3.0.0-rc.1"
rc-util "^4.5.1" rc-util "^4.5.1"
react-lifecycles-compat "^3.0.4" react-lifecycles-compat "^3.0.4"
warning "^3.0.0" warning "^3.0.0"
rc-trigger@^2.2.0, rc-trigger@^2.2.2, rc-trigger@^2.3.0, rc-trigger@^2.5.1, rc-trigger@^2.5.4: rc-trigger@^2.2.0, rc-trigger@^2.2.2, rc-trigger@^2.3.0, rc-trigger@^2.5.1, rc-trigger@^2.5.4, rc-trigger@^2.6.2:
version "2.5.4" version "2.6.2"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.5.4.tgz#9088a24ba5a811b254f742f004e38a9e2f8843fb" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.2.tgz#a9c09ba5fad63af3b2ec46349c7db6cb46657001"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.6" classnames "^2.2.6"
@ -7211,9 +7223,9 @@ rc-trigger@^3.0.0-rc.2:
rc-animate "^3.0.0-rc.1" rc-animate "^3.0.0-rc.1"
rc-util "^4.4.0" rc-util "^4.4.0"
rc-upload@~2.5.0: rc-upload@~2.6.0:
version "2.5.1" version "2.6.0"
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-2.5.1.tgz#7ae0c9038d98ba8750e9466d8f969e1b4bc9f0e0" resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-2.6.0.tgz#9cfb8dda8b1bbae823a076d2dd81a5c0b3f0aa00"
dependencies: dependencies:
babel-runtime "6.x" babel-runtime "6.x"
classnames "^2.2.5" classnames "^2.2.5"
@ -7221,10 +7233,10 @@ rc-upload@~2.5.0:
warning "2.x" warning "2.x"
rc-util@^4.0.4, rc-util@^4.1.0, rc-util@^4.1.1, rc-util@^4.3.0, rc-util@^4.4.0, rc-util@^4.5.0, rc-util@^4.5.1: rc-util@^4.0.4, rc-util@^4.1.0, rc-util@^4.1.1, rc-util@^4.3.0, rc-util@^4.4.0, rc-util@^4.5.0, rc-util@^4.5.1:
version "4.5.1" version "4.6.0"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.5.1.tgz#0e435057174c024901c7600ba8903dd03da3ab39" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.6.0.tgz#ba33721783192ec4f3afb259e182b04e55deb7f6"
dependencies: dependencies:
add-dom-event-listener "1.x" add-dom-event-listener "^1.1.0"
babel-runtime "6.x" babel-runtime "6.x"
prop-types "^15.5.10" prop-types "^15.5.10"
shallowequal "^0.2.2" shallowequal "^0.2.2"
@ -7293,7 +7305,7 @@ react-error-overlay@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
react-lazy-load@^3.0.12: react-lazy-load@^3.0.13:
version "3.0.13" version "3.0.13"
resolved "https://registry.yarnpkg.com/react-lazy-load/-/react-lazy-load-3.0.13.tgz#3b0a92d336d43d3f0d73cbe6f35b17050b08b824" resolved "https://registry.yarnpkg.com/react-lazy-load/-/react-lazy-load-3.0.13.tgz#3b0a92d336d43d3f0d73cbe6f35b17050b08b824"
dependencies: dependencies:
@ -7386,14 +7398,15 @@ react-scripts@1.1.4:
optionalDependencies: optionalDependencies:
fsevents "^1.1.3" fsevents "^1.1.3"
react-slick@~0.23.1: react-slick@~0.23.2:
version "0.23.1" version "0.23.2"
resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.23.1.tgz#15791c4107f0ba3a5688d5bd97b7b7ceaa0dd181" resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.23.2.tgz#8d8bdbc77a6678e8ad36f50c32578c7c0f1c54f6"
dependencies: dependencies:
classnames "^2.2.5" classnames "^2.2.5"
enquire.js "^2.1.6" enquire.js "^2.1.6"
json2mq "^0.2.0" json2mq "^0.2.0"
lodash.debounce "^4.0.8" lodash.debounce "^4.0.8"
prettier "^1.14.3"
resize-observer-polyfill "^1.5.0" resize-observer-polyfill "^1.5.0"
react@^16.4.1: react@^16.4.1:
@ -8026,7 +8039,7 @@ shallowequal@^0.2.2:
dependencies: dependencies:
lodash.keys "^3.1.2" lodash.keys "^3.1.2"
shallowequal@^1.0.1, shallowequal@^1.0.2: shallowequal@^1.0.1, shallowequal@^1.0.2, shallowequal@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
@ -8601,6 +8614,10 @@ tiny-relative-date@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
tinycolor2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -8706,8 +8723,8 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
ua-parser-js@^0.7.18: ua-parser-js@^0.7.18:
version "0.7.18" version "0.7.19"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
uglify-js@3.4.x, uglify-js@^3.0.13: uglify-js@3.4.x, uglify-js@^3.0.13:
version "3.4.4" version "3.4.4"
@ -8974,9 +8991,9 @@ warning@^3.0.0:
dependencies: dependencies:
loose-envify "^1.0.0" loose-envify "^1.0.0"
warning@^4.0.1, warning@~4.0.1: warning@^4.0.1, warning@^4.0.2, warning@~4.0.2:
version "4.0.1" version "4.0.2"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.1.tgz#66ce376b7fbfe8a887c22bdf0e7349d73d397745" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607"
dependencies: dependencies:
loose-envify "^1.0.0" loose-envify "^1.0.0"
@ -9117,8 +9134,8 @@ whatwg-fetch@2.0.3:
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
whatwg-fetch@>=0.10.0: whatwg-fetch@>=0.10.0:
version "2.0.4" version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
whatwg-url@^4.3.0: whatwg-url@^4.3.0:
version "4.8.0" version "4.8.0"

View File

@ -13,9 +13,9 @@ import (
// reqImport represents file upload import params. // reqImport represents file upload import params.
type reqImport struct { type reqImport struct {
Delim string `json:"delim"` Mode string `json:"mode"`
OverrideStatus bool `json:"override_status"` Delim string `json:"delim"`
ListIDs []int `json:"lists"` ListIDs []int `json:"lists"`
} }
// handleImportSubscribers handles the uploading and bulk importing of // handleImportSubscribers handles the uploading and bulk importing of
@ -36,6 +36,11 @@ func handleImportSubscribers(c echo.Context) error {
fmt.Sprintf("Invalid `params` field: %v", err)) fmt.Sprintf("Invalid `params` field: %v", err))
} }
if r.Mode != subimporter.ModeSubscribe && r.Mode != subimporter.ModeBlacklist {
return echo.NewHTTPError(http.StatusBadRequest,
"Invalid `mode`")
}
if len(r.Delim) != 1 { if len(r.Delim) != 1 {
return echo.NewHTTPError(http.StatusBadRequest, return echo.NewHTTPError(http.StatusBadRequest,
"`delim` should be a single character") "`delim` should be a single character")
@ -66,11 +71,10 @@ func handleImportSubscribers(c echo.Context) error {
} }
// Start the importer session. // Start the importer session.
impSess, err := app.Importer.NewSession(file.Filename, impSess, err := app.Importer.NewSession(file.Filename, r.Mode, r.ListIDs)
r.OverrideStatus,
r.ListIDs)
if err != nil { if err != nil {
return err return echo.NewHTTPError(http.StatusBadRequest,
fmt.Sprintf("Error starting import session: %v", err))
} }
go impSess.Start() go impSess.Start()
@ -81,7 +85,8 @@ func handleImportSubscribers(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error extracting ZIP file: %v", err)) fmt.Sprintf("Error extracting ZIP file: %v", err))
} else if len(files) == 0 { } else if len(files) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "No CSV files found to import.") return echo.NewHTTPError(http.StatusBadRequest,
"No CSV files found to import.")
} }
go impSess.LoadCSV(dir+"/"+files[0], rune(r.Delim[0])) go impSess.LoadCSV(dir+"/"+files[0], rune(r.Delim[0]))
@ -94,7 +99,6 @@ func handleGetImportSubscribers(c echo.Context) error {
app = c.Get("app").(*App) app = c.Get("app").(*App)
s = app.Importer.GetStats() s = app.Importer.GetStats()
) )
return c.JSON(http.StatusOK, okResp{s}) return c.JSON(http.StatusOK, okResp{s})
} }
@ -110,6 +114,5 @@ func handleGetImportSubscriberLogs(c echo.Context) error {
func handleStopImportSubscribers(c echo.Context) error { func handleStopImportSubscribers(c echo.Context) error {
app := c.Get("app").(*App) app := c.Get("app").(*App)
app.Importer.Stop() app.Importer.Stop()
return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()}) return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()})
} }

View File

@ -122,9 +122,7 @@ func install(app *App, qMap goyesql.Queries) {
uuid.NewV4(), uuid.NewV4(),
email, email,
bytes.Title(name[0]), bytes.Title(name[0]),
models.SubscriberStatusEnabled,
`{"type": "known", "good": true}`, `{"type": "known", "good": true}`,
true,
pq.Int64Array{int64(listID)}, pq.Int64Array{int64(listID)},
); err != nil { ); err != nil {
logger.Fatalf("Error creating subscriber: %v", err) logger.Fatalf("Error creating subscriber: %v", err)

View File

@ -221,7 +221,7 @@ func main() {
} }
app.Queries = q app.Queries = q
app.Importer = subimporter.New(q.UpsertSubscriber.Stmt, db.DB) app.Importer = subimporter.New(q.UpsertSubscriber.Stmt, q.BlacklistSubscriber.Stmt, db.DB)
// Campaign daemon. // Campaign daemon.
r := runner.New(runner.Config{ r := runner.New(runner.Config{

View File

@ -9,6 +9,7 @@ import (
// Queries contains all prepared SQL queries. // Queries contains all prepared SQL queries.
type Queries struct { type Queries struct {
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"` UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
BlacklistSubscriber *sqlx.Stmt `query:"blacklist-subscriber"`
GetSubscriber *sqlx.Stmt `query:"get-subscriber"` GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"` GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"` GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`

View File

@ -40,19 +40,35 @@ SELECT COUNT(subscribers.id) as num FROM subscribers INNER JOIN subscriber_lists
%s; %s;
-- name: upsert-subscriber -- name: upsert-subscriber
-- In case of updates, if $6 (override_status) is true, only then, the existing -- Upserts a subscriber where existing subscribers get their names and attributes overwritten.
-- value is overwritten with the incoming value. This is used for insertions and bulk imports. -- The status field is only updated when $6 = 'override_status'.
WITH s AS ( WITH s AS (
INSERT INTO subscribers (uuid, email, name, status, attribs) INSERT INTO subscribers (uuid, email, name, attribs)
VALUES($1, $2, $3, $4, $5) ON CONFLICT (email) DO UPDATE VALUES($1, $2, $3, $4)
SET name=$3, status=(CASE WHEN $6 = true THEN $4 ELSE subscribers.status END), ON CONFLICT (email) DO UPDATE
attribs=$5, updated_at=NOW() SET name=$3,
attribs=$4,
updated_at=NOW()
RETURNING id RETURNING id
) INSERT INTO subscriber_lists (subscriber_id, list_id) ) INSERT INTO subscriber_lists (subscriber_id, list_id)
VALUES((SELECT id FROM s), UNNEST($7::INT[]) ) VALUES((SELECT id FROM s), UNNEST($5::INT[]))
ON CONFLICT (subscriber_id, list_id) DO NOTHING ON CONFLICT (subscriber_id, list_id) DO UPDATE
SET updated_at=NOW()
RETURNING subscriber_id; RETURNING subscriber_id;
-- name: blacklist-subscriber
-- Upserts a subscriber where the update will only set the status to blacklisted
-- unlike upsert-subscribers where name and attributes are updated. In addition, all
-- existing subscriptions are marked as 'unsubscribed'.
WITH sub AS (
INSERT INTO subscribers (uuid, email, name, attribs, status)
VALUES($1, $2, $3, $4, 'blacklisted')
ON CONFLICT (email) DO UPDATE SET status='blacklisted', updated_at=NOW()
RETURNING id
)
UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
WHERE subscriber_id = (SELECT id FROM sub);
-- name: update-subscriber -- name: update-subscriber
-- Updates a subscriber's data, and given a list of list_ids, inserts subscriptions -- Updates a subscriber's data, and given a list of list_ids, inserts subscriptions
-- for them while deleting existing subscriptions not in the list. -- for them while deleting existing subscriptions not in the list.

View File

@ -29,7 +29,7 @@ CREATE TABLE subscribers (
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
attribs JSONB, attribs JSONB,
status subscriber_status NOT NULL, status subscriber_status NOT NULL DEFAULT 'enabled',
campaigns INTEGER[], campaigns INTEGER[],
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

View File

@ -46,11 +46,15 @@ const (
StatusStopping = "stopping" StatusStopping = "stopping"
StatusFinished = "finished" StatusFinished = "finished"
StatusFailed = "failed" StatusFailed = "failed"
ModeSubscribe = "subscribe"
ModeBlacklist = "blacklist"
) )
// Importer represents the bulk CSV subscriber import system. // Importer represents the bulk CSV subscriber import system.
type Importer struct { type Importer struct {
stmt *sql.Stmt upsert *sql.Stmt
blacklist *sql.Stmt
db *sql.DB db *sql.DB
isImporting bool isImporting bool
stop chan bool stop chan bool
@ -65,8 +69,8 @@ type Session struct {
subQueue chan SubReq subQueue chan SubReq
log *log.Logger log *log.Logger
overrideStatus bool mode string
listIDs []int listIDs []int
} }
// Status reporesents statistics from an ongoing import session. // Status reporesents statistics from an ongoing import session.
@ -91,17 +95,17 @@ var (
csvHeaders = map[string]bool{"email": true, csvHeaders = map[string]bool{"email": true,
"name": true, "name": true,
"status": true,
"attributes": true} "attributes": true}
) )
// New returns a new instance of Importer. // New returns a new instance of Importer.
func New(stmt *sql.Stmt, db *sql.DB) *Importer { func New(upsert *sql.Stmt, blacklist *sql.Stmt, db *sql.DB) *Importer {
im := Importer{ im := Importer{
stmt: stmt, upsert: upsert,
stop: make(chan bool, 1), blacklist: blacklist,
db: db, stop: make(chan bool, 1),
status: &Status{Status: StatusNone, logBuf: bytes.NewBuffer(nil)}, db: db,
status: &Status{Status: StatusNone, logBuf: bytes.NewBuffer(nil)},
} }
return &im return &im
@ -109,7 +113,7 @@ func New(stmt *sql.Stmt, db *sql.DB) *Importer {
// NewSession returns an new instance of Session. It takes the name // NewSession returns an new instance of Session. It takes the name
// of the uploaded file, but doesn't do anything with it but retains it for stats. // of the uploaded file, but doesn't do anything with it but retains it for stats.
func (im *Importer) NewSession(fName string, overrideStatus bool, listIDs []int) (*Session, error) { func (im *Importer) NewSession(fName, mode string, listIDs []int) (*Session, error) {
if im.getStatus() != StatusNone { if im.getStatus() != StatusNone {
return nil, errors.New("an import is already running") return nil, errors.New("an import is already running")
} }
@ -121,11 +125,11 @@ func (im *Importer) NewSession(fName string, overrideStatus bool, listIDs []int)
im.Unlock() im.Unlock()
s := &Session{ s := &Session{
im: im, im: im,
log: log.New(im.status.logBuf, "", log.Ldate|log.Ltime), log: log.New(im.status.logBuf, "", log.Ldate|log.Ltime),
subQueue: make(chan SubReq, commitBatchSize), subQueue: make(chan SubReq, commitBatchSize),
overrideStatus: overrideStatus, mode: mode,
listIDs: listIDs, listIDs: listIDs,
} }
s.log.Printf("processing '%s'", fName) s.log.Printf("processing '%s'", fName)
@ -136,7 +140,6 @@ func (im *Importer) NewSession(fName string, overrideStatus bool, listIDs []int)
func (im *Importer) GetStats() Status { func (im *Importer) GetStats() Status {
im.RLock() im.RLock()
defer im.RUnlock() defer im.RUnlock()
return Status{ return Status{
Name: im.status.Name, Name: im.status.Name,
Status: im.status.Status, Status: im.status.Status,
@ -206,17 +209,20 @@ func (s *Session) Start() {
s.log.Printf("error creating DB transaction: %v", err) s.log.Printf("error creating DB transaction: %v", err)
continue continue
} }
stmt = tx.Stmt(s.im.stmt)
if s.mode == ModeSubscribe {
stmt = tx.Stmt(s.im.upsert)
} else {
stmt = tx.Stmt(s.im.blacklist)
}
} }
_, err := stmt.Exec( var err error
uuid.NewV4(), if s.mode == ModeSubscribe {
sub.Email, _, err = stmt.Exec(uuid.NewV4(), sub.Email, sub.Name, sub.Attribs, listIDs)
sub.Name, } else if s.mode == ModeBlacklist {
sub.Status, _, err = stmt.Exec(uuid.NewV4(), sub.Email, sub.Name, sub.Attribs)
sub.Attribs, }
s.overrideStatus,
listIDs)
if err != nil { if err != nil {
s.log.Printf("error executing insert: %v", err) s.log.Printf("error executing insert: %v", err)
tx.Rollback() tx.Rollback()
@ -403,8 +409,13 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error {
if err == io.EOF { if err == io.EOF {
break break
} else if err != nil { } else if err != nil {
s.log.Printf("error reading CSV '%s'", err) if err, ok := err.(*csv.ParseError); ok && err.Err == csv.ErrFieldCount {
return err s.log.Printf("skipping line %d. %v", i, err)
continue
} else {
s.log.Printf("error reading CSV '%s'", err)
return err
}
} }
lnCols := len(cols) lnCols := len(cols)
@ -424,13 +435,6 @@ func (s *Session) LoadCSV(srcPath string, delim rune) error {
// Lowercase to ensure uniqueness in the DB. // Lowercase to ensure uniqueness in the DB.
sub.Email = strings.ToLower(strings.TrimSpace(row["email"])) sub.Email = strings.ToLower(strings.TrimSpace(row["email"]))
sub.Name = row["name"] sub.Name = row["name"]
if _, ok := row["status"]; ok {
sub.Status = row["status"]
} else {
sub.Status = models.SubscriberStatusEnabled
}
if err := ValidateFields(sub); err != nil { if err := ValidateFields(sub); err != nil {
s.log.Printf("skipping line %d: %v", i, err) s.log.Printf("skipping line %d: %v", i, err)
continue continue
@ -502,10 +506,6 @@ func ValidateFields(s SubReq) error {
if !govalidator.IsByteLength(s.Name, 1, stdInputMaxLen) { if !govalidator.IsByteLength(s.Name, 1, stdInputMaxLen) {
return errors.New("invalid or empty `name`") return errors.New("invalid or empty `name`")
} }
if s.Status != SubscriberStatusEnabled && s.Status != SubscriberStatusDisabled &&
s.Status != SubscriberStatusBlacklisted {
return errors.New("invalid `status`")
}
return nil return nil
} }