Added subs management (wip)

This commit is contained in:
vjrj 2017-12-16 18:47:08 +01:00
parent 6123977974
commit 316e14f0ea
21 changed files with 427 additions and 39 deletions

View file

@ -48,3 +48,4 @@ ostrio:mailer # mailer
ostrio:meteor-root
mizzao:user-status
babrahams:constellation
percolate:migrations

View file

@ -104,6 +104,7 @@ peerlibrary:assert@0.2.5
peerlibrary:fiber-utils@0.6.0
peerlibrary:reactive-mongo@0.1.1
peerlibrary:server-autorun@0.5.2
percolate:migrations@1.0.2
promise@0.10.0
raix:eventemitter@0.1.3
random@1.0.10

View file

@ -3,7 +3,7 @@
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
const ActiveFires = new Mongo.Collection('activefires');
const ActiveFires = new Mongo.Collection('activefires', { idGeneration: 'MONGO' });
ActiveFires.allow({
insert: () => false,

View file

@ -3,18 +3,18 @@
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
const FireAlerts = new Mongo.Collection('avisosfuego');
const FireAlerts = new Mongo.Collection('avisosfuego', { idGeneration: 'MONGO' });
FireAlerts.allow({
insert: () => false,
update: () => false,
remove: () => false,
remove: () => false
});
FireAlerts.deny({
insert: () => true,
update: () => true,
remove: () => true,
remove: () => true
});
/* Sample:
@ -42,9 +42,8 @@ FireAlerts.schema = new SimpleSchema({
location: Object,
'location.lat': SimpleSchema.Integer,
'location.lon': SimpleSchema.Integer,
aviso: Date,
}
);
aviso: Date
});
FireAlerts.attachSchema(FireAlerts.schema);

View file

@ -3,7 +3,7 @@
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
const Subscriptions = new Mongo.Collection('subscriptions');
const Subscriptions = new Mongo.Collection('subscriptions', { idGeneration: 'MONGO' });
Subscriptions.allow({
insert: () => false,
@ -41,9 +41,10 @@ Subscriptions.schema = new SimpleSchema({
location: Object,
'location.lat': SimpleSchema.Integer,
'location.lon': SimpleSchema.Integer,
distance: Number
distance: Number,
owner: String
});
Subscriptions.attachSchema(Subscriptions.schema);
// Subscriptions.attachSchema(Subscriptions.schema);
export default Subscriptions;

View file

@ -0,0 +1,53 @@
import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import Subscriptions from './Subscriptions';
import rateLimit from '../../modules/rate-limit';
Meteor.methods({
'subscriptions.insert': function subscriptionsInsert(doc) {
check(doc, {
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
distance: Number
});
try {
return Subscriptions.insert({ owner: this.userId, ...doc });
} catch (exception) {
throw new Meteor.Error('500', exception);
}
},
'subscriptions.update': function subscriptionsUpdate(doc) {
check(doc, {
_id: String,
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
distance: Number
});
try {
const subscriptionId = doc._id;
Subscriptions.update(subscriptionId, { $set: doc });
return subscriptionId; // Return _id so we can redirect to subscription after update.
} catch (exception) {
throw new Meteor.Error('500', exception);
}
},
'subscriptions.remove': function subscriptionsRemove(subscriptionId) {
check(subscriptionId, Meteor.Collection.ObjectID);
try {
return Subscriptions.remove(subscriptionId);
} catch (exception) {
throw new Meteor.Error('500', exception);
}
}
});
rateLimit({
methods: [
'subscriptions.insert',
'subscriptions.update',
'subscriptions.remove'
],
limit: 5,
timeRange: 1000
});

View file

@ -34,3 +34,13 @@ Meteor.publishTransformed('userSubsToFires', function transform() {
return doc;
});
});
Meteor.publish('subscriptions', function subscriptions() {
return Subscriptions.find({ owner: this.userId });
});
// Note: subscriptions.view is also used when editing an existing subscription.
Meteor.publish('subscriptions.view', function subscriptionsView(subscriptionId) {
check(subscriptionId, String);
return Subscriptions.find({ _id: subscriptionId, owner: this.userId });
});

View file

@ -10,5 +10,7 @@ import '../../api/Utility/server/methods';
import '../../api/ActiveFires/server/publications';
import '../../api/FireAlerts/server/publications';
import '../../api/Subscriptions/methods';
import '../../api/Subscriptions/server/publications';
// TODO add rate-limit to these publications

View file

@ -19,6 +19,9 @@ const AuthenticatedNavigation = ({ name, history, props }) => (
<LinkContainer className="nav-item" anchorClassName="nav-link" to="/documents">
<NavItem eventKey={5} href="/documents">Documents</NavItem>
</LinkContainer>
<LinkContainer className="nav-item" anchorClassName="nav-link" to="/documents">
<NavItem eventKey={5.01} href="/subscriptions">Subscriptions</NavItem>
</LinkContainer>
<LinkContainer className="nav-item" anchorClassName="nav-link" to="/profile">
<NavItem eventKey={5.1} href="/profile">{name}</NavItem>
</LinkContainer>
@ -30,7 +33,7 @@ const AuthenticatedNavigation = ({ name, history, props }) => (
);
AuthenticatedNavigation.propTypes = {
name: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
};

View file

@ -33,7 +33,7 @@ class SelectionMap extends Component {
this.updatePosition = this.updatePosition.bind(this);
this.fit = this.fit.bind(this);
this.addScale = this.addScale.bind(this);
this.doSubs = this.doSubs.bind(this);
this.onSubs = this.onSubs.bind(this);
}
componentDidMount() {
@ -57,6 +57,13 @@ class SelectionMap extends Component {
this.fit();
}
onSubs() {
this.props.onSubs({
location: { lat: this.state.center[0], lon: this.state.center[1] },
distance: this.state.distance
});
}
getMap() {
return this.selectionMap.leafletElement;
}
@ -92,13 +99,6 @@ class SelectionMap extends Component {
Leaflet.control.graphicScale([options]).addTo(map);
}
doSubs() { // event param not used
this.props.history.push(
'/login',
{ subscription: { center: this.state.center, distance: this.state.distance } }
);
}
isValidState() {
return this.state.center && this.state.center[0] && this.state.distance;
}
@ -152,9 +152,9 @@ class SelectionMap extends Component {
<ButtonToolbar>
<Button
bsStyle="success"
onClick={event => this.doSubs(event)}
onClick={event => this.onSubs(event)}
>
{this.props.t('Subscribirme a fuegos en este rádio')}
{this.props.subsBtn}
</Button>
</ButtonToolbar>
</Control>
@ -165,11 +165,12 @@ class SelectionMap extends Component {
}
SelectionMap.propTypes = {
history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
center: PropTypes.array,
center: PropTypes.arrayOf(PropTypes.number),
distance: PropTypes.number,
onSelection: PropTypes.func.isRequired
onSelection: PropTypes.func.isRequired,
subsBtn: PropTypes.string.isRequired,
onSubs: PropTypes.func.isRequired
};
export default translate([], { wait: true })(withTracker(props => ({

View file

@ -0,0 +1,60 @@
/* eslint-disable max-len, no-return-assign */
/* eslint-disable react/jsx-indent */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */
import React from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { Bert } from 'meteor/themeteorchef:bert';
import FireSubscription from '/imports/ui/pages/FireSubscription/FireSubscription';
import { translate } from 'react-i18next';
class SubscriptionEditor extends React.Component {
onSubs(value) {
const { t, history } = this.props;
const existingSubscription = this.props.doc && this.props.doc._id;
const methodToCall = existingSubscription ? 'subscriptions.update' : 'subscriptions.insert';
const doc = {
location: value.location,
distance: value.distance
};
if (existingSubscription) doc._id = existingSubscription;
Meteor.call(methodToCall, doc, (error, subscriptionId) => {
if (error) {
Bert.alert(error.reason, 'danger');
} else {
const confirmation = existingSubscription ? t('Suscripción actualizada') : t('Suscripción añadida');
Bert.alert(confirmation, 'success');
history.push(`/subscriptions/${subscriptionId}`);
}
});
}
render() {
const { doc, t } = this.props;
const isEdit = doc && doc._id;
return (
<FireSubscription
center={[doc.location.lat, doc.location.lon]}
distance={doc.distance}
focusInput={!isEdit}
subsBtn={isEdit ? t('Actualizar') : t('Subscribirme a fuegos en este rádio')}
onSubs={state => this.onSubs(state)}
/>
);
}
}
SubscriptionEditor.defaultProps = {
doc: { location: { lat: null, lot: null }, distance: null }
};
SubscriptionEditor.propTypes = {
doc: PropTypes.object,
t: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
};
export default translate([], { wait: true })(SubscriptionEditor);

View file

@ -28,6 +28,10 @@ import Documents from '../../pages/Documents/Documents';
import NewDocument from '../../pages/NewDocument/NewDocument';
import ViewDocument from '../../pages/ViewDocument/ViewDocument';
import EditDocument from '../../pages/EditDocument/EditDocument';
import Subscriptions from '../../pages/Subscriptions/Subscriptions';
import NewSubscription from '../../pages/NewSubscription/NewSubscription';
import ViewSubscription from '../../pages/ViewSubscription/ViewSubscription';
import EditSubscription from '../../pages/EditSubscription/EditSubscription';
import Signup from '../../pages/Signup/Signup';
import Login from '../../pages/Login/Login';
import Logout from '../../pages/Logout/Logout';
@ -69,13 +73,20 @@ const App = props => (
<Authenticated exact path="/documents/new" component={NewDocument} {...props} />
<Authenticated exact path="/documents/:_id" component={ViewDocument} {...props} />
<Authenticated exact path="/documents/:_id/edit" component={EditDocument} {...props} />
<Authenticated exact path="/subscriptions" component={Subscriptions} {...props} />
<Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} />
<Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} />
<Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} />
<Authenticated exact path="/profile" component={Profile} {...props} />
<Route path="/fires" component={FiresMap} {...props} />
<Public path="/signup" component={Signup} {...props} />
<Public path="/login" component={Login} {...props} />
<Route path="/logout" component={Logout} {...props} />
<Route path="/sandbox" component={Sandbox} {...props} />
<Route path="/subscriptions" render={props => <FireSubscription focusInput {...props} />} />
{/* <Route path="/subscriptions" render={props => <FireSubscription focusInput {...props} />} /> */}
<Route name="verify-email" path="/verify-email/:token" component={VerifyEmail} />
<Route name="recover-password" path="/recover-password" component={RecoverPassword} />
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />

View file

@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createContainer } from 'meteor/react-meteor-data';
import { Meteor } from 'meteor/meteor';
import Subscriptions from '../../../api/Subscriptions/Subscriptions';
import SubscriptionEditor from '../../components/SubscriptionEditor/SubscriptionEditor';
import NotFound from '../NotFound/NotFound';
const EditSubscription = ({ doc, history }) => (doc ? (
<div className="EditSubscription">
<h4 className="page-header">{`Editing "${doc.title}"`}</h4>
<SubscriptionEditor doc={doc} history={history} />
</div>
) : <NotFound />);
EditSubscription.defaultProps = {
doc: null
};
EditSubscription.propTypes = {
doc: PropTypes.object,
history: PropTypes.object.isRequired
};
export default createContainer(({ match }) => {
const subscriptionId = match.params._id;
const subscription = Meteor.subscribe('subscriptions.view', subscriptionId);
return {
loading: !subscription.ready(),
doc: Subscriptions.findOne(subscriptionId)
};
}, EditSubscription);

View file

@ -3,7 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col } from 'react-bootstrap';
import { Trans, translate } from 'react-i18next';
import { translate } from 'react-i18next';
import DistanceSlider from '/imports/ui/components/DistanceSlider/DistanceSlider';
import SelectionMap from '/imports/ui/components/SelectionMap/SelectionMap';
import Gkeys from '/imports/startup/client/Gkeys';
@ -14,7 +14,9 @@ class FireSubscription extends React.Component {
constructor(props) {
super(props);
this.state = {
init: false
init: false,
center: this.props.center,
distance: this.props.distance
};
// console.log(this.props.location.state);
}
@ -27,7 +29,7 @@ class FireSubscription extends React.Component {
}
onAutocompleteChange(value) {
this.setState({ lat: value.lat, lng: value.lng });
this.setState({ center: [value.lat, value.lng] });
}
onSliderChange(value) {
@ -35,11 +37,15 @@ class FireSubscription extends React.Component {
}
onSelection(value) {
this.setState({ lat: value.lat, lng: value.lng, distance: value.distance });
this.setState({ center: [value.lat, value.lng], distance: value.distance });
}
onSubs(value) {
this.props.onSubs(value);
}
centerOnUserLocation(value) {
this.setState({ lat: value.center[0], lng: value.center[1] });
this.setState({ center: value.center });
}
render() {
@ -54,7 +60,6 @@ class FireSubscription extends React.Component {
<Row>
<Col xs={12} sm={12} md={6} lg={6} >
<div>
<h4 className="page-header"><Trans parent="span">Suscríbete a alertas de fuegos</Trans></h4>
<SubsAutocomplete
focusInput={this.props.focusInput}
onChange={value => this.onAutocompleteChange(value)}
@ -70,10 +75,11 @@ class FireSubscription extends React.Component {
</Row>
<Row className="align-items-center justify-content-center">
<SelectionMap
center={[this.state.lat, this.state.lng]}
center={this.state.center}
distance={this.state.distance}
history={this.props.history}
subsBtn={this.props.subsBtn}
onSelection={state => this.onSelection(state)}
onSubs={state => this.onSubs(state)}
/>
</Row>
</div>
@ -82,8 +88,12 @@ class FireSubscription extends React.Component {
}
FireSubscription.propTypes = {
history: PropTypes.object.isRequired,
focusInput: PropTypes.bool.isRequired
t: PropTypes.func.isRequired,
center: PropTypes.arrayOf(PropTypes.number),
distance: PropTypes.number,
focusInput: PropTypes.bool.isRequired,
subsBtn: PropTypes.string.isRequired,
onSubs: PropTypes.func.isRequired
};
export default translate([], { wait: true })(FireSubscription);

View file

@ -12,7 +12,7 @@ import ReactResizeDetector from 'react-resize-detector';
import _ from 'lodash';
import { ScrollToTopOnMount, SectionsContainer, Section } from 'react-fullpage';
import 'html5-device-mockups/dist/device-mockups.min.css';
import FireSubscription from '/imports/ui/pages/FireSubscription/FireSubscription';
import SubscriptionEditor from '/imports/ui/components/SubscriptionEditor/SubscriptionEditor';
import FiresMap from '../FiresMap/FiresMap';
import './Index.scss';
@ -218,7 +218,8 @@ class Index extends Component {
<Section className="">
<div className="container">
<FireSubscription history={this.props.history} focusInput={false} />
<h4 className="page-header"><Trans parent="span">Suscríbete a alertas de fuegos</Trans></h4>
<SubscriptionEditor history={this.props.history} />
</div>
<div className="overlay" />
</Section>

View file

@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, translate } from 'react-i18next';
import SubscriptionEditor from '../../components/SubscriptionEditor/SubscriptionEditor';
const NewSubscription = ({ history }) => (
<div className="NewSubscription">
<h4 className="page-header"><Trans>Nueva suscripción</Trans></h4>
<SubscriptionEditor history={history} />
</div>
);
NewSubscription.propTypes = {
history: PropTypes.object.isRequired
};
export default translate([], { wait: true })(NewSubscription);

View file

@ -0,0 +1,105 @@
/* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent */
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Table, Alert, Button, Row } from 'react-bootstrap';
import { timeago, monthDayYearAtTime } from '@cleverbeagle/dates';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { Trans, translate } from 'react-i18next';
import { Bert } from 'meteor/themeteorchef:bert';
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
import Loading from '../../components/Loading/Loading';
import './Subscriptions.scss';
const handleRemove = (subscriptionId) => {
if (confirm('Are you sure? This is permanent!')) {
Meteor.call('subscriptions.remove', subscriptionId, (error) => {
if (error) {
Bert.alert(error.reason, 'danger');
} else {
Bert.alert('Subscription deleted!', 'success');
}
});
}
};
const Subscriptions = ({
loading,
subscriptions,
match,
history
}) => (!loading ? (
<div className="Subscriptions">
<div className="page-header clearfix">
<h4 className="pull-left"><Trans>Suscripciones a fuegos en áreas de mi interés</Trans></h4>
<Link className="btn btn-success pull-right" to={`${match.url}/new`}><Trans>Añadir suscripción</Trans></Link>
</div>
<br />
{subscriptions.length ?
<Table responsive>
<thead>
<tr>
<th>Location</th>
<th>Last Updated</th>
<th>Created</th>
<th />
<th />
</tr>
</thead>
<tbody>
{subscriptions.map(({
_id,
location,
createdAt,
updatedAt
}) => (
<tr key={_id}>
<td>{location.lat},{location.lon}</td>
<td>{timeago(updatedAt)}</td>
<td>{monthDayYearAtTime(createdAt)}</td>
<td>
<Button
bsStyle="primary"
onClick={() => history.push(`${match.url}/${_id}`)}
block
>View
</Button>
</td>
<td>
<Button
bsStyle="danger"
onClick={() => handleRemove(_id)}
block
>Delete
</Button>
</td>
</tr>
))}
</tbody>
</Table> :
<Alert bsStyle="warning"><Trans>Todavía sin suscriptiones</Trans></Alert>
}
</div>
) : <Loading />);
Subscriptions.propTypes = {
loading: PropTypes.bool.isRequired,
subscriptions: PropTypes.arrayOf(PropTypes.object).isRequired,
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default translate([], { wait: true })(withTracker(() => {
const subscription = Meteor.subscribe('subscriptions');
// console.log(UserSubsToFiresCollection.find().fetch());
return {
loading: !subscription.ready(),
subscriptions: UserSubsToFiresCollection.find().fetch()
};
})(Subscriptions));

View file

@ -0,0 +1,3 @@
.Subscriptions table tbody tr td {
vertical-align: middle;
}

View file

@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap';
import { createContainer } from 'meteor/react-meteor-data';
import { Meteor } from 'meteor/meteor';
import { Bert } from 'meteor/themeteorchef:bert';
import Subscriptions from '../../../api/Subscriptions/Subscriptions';
import NotFound from '../NotFound/NotFound';
import Loading from '../../components/Loading/Loading';
const handleRemove = (subscriptionId, history) => {
if (confirm('Are you sure? This is permanent!')) {
Meteor.call('subscriptions.remove', subscriptionId, (error) => {
if (error) {
Bert.alert(error.reason, 'danger');
} else {
Bert.alert('Subscription deleted!', 'success');
history.push('/subscriptions');
}
});
}
};
const renderSubscription = (doc, match, history) => (doc ? (
<div className="ViewSubscription">
<div className="page-header clearfix">
<h4 className="pull-left">{ doc && doc.title }</h4>
<ButtonToolbar className="pull-right">
<ButtonGroup bsSize="small">
<Button onClick={() => history.push(`${match.url}/edit`)}>Edit</Button>
<Button onClick={() => handleRemove(doc._id, history)} className="text-danger">
Delete
</Button>
</ButtonGroup>
</ButtonToolbar>
</div>
{ doc && doc.body }
</div>
) : <NotFound />);
const ViewSubscription = ({
loading,
doc,
match,
history
}) => (
!loading ? renderSubscription(doc, match, history) : <Loading />
);
ViewSubscription.propTypes = {
loading: PropTypes.bool.isRequired,
doc: PropTypes.object,
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default createContainer(({ match }) => {
const subscriptionId = match.params._id;
const subscription = Meteor.subscribe('subscriptions.view', subscriptionId);
return {
loading: !subscription.ready(),
doc: Subscriptions.findOne(subscriptionId)
};
}, ViewSubscription);

View file

@ -104,5 +104,11 @@
"Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.":
"Images captured by NASA satellites show the smoke of large fires spreading over the Pacific Ocean. Fire activity is outlined in red.",
"Colaboración masiva contra los incendios":
"Corwdsourcing against wildfires"
"Corwdsourcing against wildfires",
"Todavía sin subscriptiones":
"No subscriptions yet",
"Suscripción añadida":
"Subscription added",
"Suscripción actualizada":
"Subscription updated"
}

View file

@ -131,5 +131,11 @@
"Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.":
"Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.",
"Colaboración masiva contra los incendios":
"Colaboración masiva contra los incendios"
"Colaboración masiva contra los incendios",
"Todavía sin subscriptiones":
"Todavía sin subscriptiones",
"Suscripción añadida":
"Suscripción añadida",
"Suscripción actualizada":
"Suscripción actualizada"
}