Site settings and fires fromNow Reactive

This commit is contained in:
vjrj 2018-01-29 11:30:17 +01:00
parent e4b0135953
commit 379e72a9e7
15 changed files with 482 additions and 64 deletions

View file

@ -60,4 +60,3 @@ mongo-livedata
reywood:publish-composite reywood:publish-composite
barbatus:stars-rating barbatus:stars-rating
arkham:comments-ui arkham:comments-ui
remcoder:chronos

View file

@ -111,7 +111,6 @@ react-meteor-data@0.2.15
reactive-dict@1.2.0 reactive-dict@1.2.0
reactive-var@1.0.11 reactive-var@1.0.11
reload@1.1.11 reload@1.1.11
remcoder:chronos@0.5.0
retry@1.0.9 retry@1.0.9
reywood:publish-composite@1.5.2 reywood:publish-composite@1.5.2
routepolicy@1.0.12 routepolicy@1.0.12
@ -138,4 +137,3 @@ url@1.1.0
vjrj:piwik@0.3.1 vjrj:piwik@0.3.1
webapp@1.4.0 webapp@1.4.0
webapp-hashing@1.0.9 webapp-hashing@1.0.9
zodiase:function-bind@0.0.1

View file

@ -0,0 +1,49 @@
/* eslint-disable consistent-return */
/* eslint-disable import/no-absolute-path */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
import SiteSettingsTypes from './SiteSettingsTypes';
const SiteSettings = new Mongo.Collection('siteSettings', { idGeneration: 'MONGO' });
SiteSettings.allow({
insert: () => false,
update: () => false,
remove: () => false
});
SiteSettings.deny({
insert: () => true,
update: () => true,
remove: () => true
});
SiteSettings.get = (name) => {
const setting = SiteSettings.findOne({
name
});
return typeof setting === 'object' ? setting.value : undefined;
};
SiteSettings.observe = (name, callback) => {
SiteSettings.find({ name }).observe({
added: (document) => {
callback(document.value);
}
});
};
SiteSettings.getSchema = type => new SimpleSchema({
name: String,
type: String,
description: String,
value: SiteSettingsTypes[type].value,
createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt
});
// SiteSettings.attachSchema(SiteSettings.schema);
export default SiteSettings;

View file

@ -0,0 +1,66 @@
// Adapted from: https://github.com/yogiben/meteor-admin-settings
const SiteSettingsTypes = {
string: {
value: {
type: String
}
},
textarea: {
value: {
type: String
/* autoform: {
* afFieldInput: {
* rows: 5
* }
* } */
}
},
number: {
value: {
type: Number
}
},
boolean: {
value: {
type: Boolean
/* autoform: {
* afFieldInput: {
* type: 'boolean-select'
* }
* } */
}
},
date: {
value: {
type: Date
/* autoform: {
* afFieldInput: {
* type: 'date'
* }
* } */
}
},
color: {
value: {
type: String
/* autoform: {
* afFieldInput: {
* type: 'color'
* }
* } */
}
},
password: {
value: {
type: String
/* autoform: {
* afFieldInput: {
* type: 'password'
* }
* } */
}
}
};
export default SiteSettingsTypes;

View file

@ -0,0 +1,60 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import SiteSettings from './SiteSettings';
import SiteSettingsTypes from './SiteSettingsTypes';
import rateLimit from '../../modules/rate-limit';
Meteor.methods({
'siteSettings.insert': function siteSettingsInsert(setting) {
check(setting, {
name: String,
type: String,
description: String,
value: SiteSettingsTypes[self.type].value
});
SiteSettings.getSchema.validate(setting);
try {
return SiteSettings.insert({ owner: this.userId, ...setting });
} catch (exception) {
throw new Meteor.Error('500', exception);
}
},
'siteSettings.update': function siteSettingsUpdate(setting) {
check(setting, {
_id: String,
name: String,
type: String,
description: String,
value: SiteSettingsTypes[self.type].value
});
try {
const siteSettingId = setting._id;
SiteSettings.update(siteSettingId, { $set: setting });
return siteSettingId; // Return _id so we can redirect to siteSetting after update.
} catch (exception) {
throw new Meteor.Error('500', exception);
}
},
'siteSettings.remove': function siteSettingsRemove(siteSettingId) {
check(siteSettingId, String);
try {
return SiteSettings.remove(siteSettingId);
} catch (exception) {
throw new Meteor.Error('500', exception);
}
}
});
rateLimit({
methods: [
'siteSettings.insert',
'siteSettings.update',
'siteSettings.remove'
],
limit: 5,
timeRange: 1000
});

View file

@ -0,0 +1,6 @@
/* eslint-disable prefer-arrow-callback */
import { Meteor } from 'meteor/meteor';
import SiteSettings from '../SiteSettings';
Meteor.publish('settings', () => SiteSettings.find());

View file

@ -21,3 +21,6 @@ import '../../api/Notifications/server/publications';
import '../../api/Fires/methods'; import '../../api/Fires/methods';
import '../../api/Fires/server/publications'; import '../../api/Fires/server/publications';
import '../../api/SiteSettings/methods';
import '../../api/SiteSettings/server/publications';

View file

@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base';
import randomHex from 'crypto-random-hex'; import randomHex from 'crypto-random-hex';
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts'; import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
Meteor.startup(() => { Meteor.startup(() => {
// https://github.com/percolatestudio/meteor-migrations // https://github.com/percolatestudio/meteor-migrations
@ -83,8 +84,16 @@ Meteor.startup(() => {
} }
}); });
Migrations.add({
version: 5,
up: function siteSettingsIndex() {
// other way:
SiteSettings._ensureIndex({ name: 1 }, { unique: 1 });
}
});
// Set createdAt in users & subs // Set createdAt in users & subs
Migrations.migrateTo('latest'); Migrations.migrateTo('latest');
// Migrations.migrateTo('2,rerun'); // Migrations.migrateTo('5,rerun');
}); });

View file

@ -0,0 +1,122 @@
// Adapted from: https://github.com/remcoder/chronos (MIT License)
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import moment from 'moment';
const _timers = {};
function Timer(interval) {
this.interval = interval || 1000;
this.time = new ReactiveVar(0);
}
Timer.prototype.start = function () {
if (this._timer) throw new Error('Trying to start Chronos.Timer but it is already running.');
this.time.set(new Date());
this._timer = setInterval(Meteor.bindEnvironment(() => {
// console.log('tick', this._timer);
this.time.set(new Date());
}), this.interval);
};
Timer.prototype.stop = function () {
// console.log('stopping timer');
clearInterval(this._timer);
this._timer = null;
};
function _update(interval) {
// get current reactive context
const comp = Tracker.currentComputation;
if (!comp) { return; } // no nothing when used outside a reactive context
// only create one timer per reactive context to prevent stacking of timers
const cid = comp && comp._id;
if (!_timers[cid]) {
const timer = new Timer(interval);
_timers[cid] = timer;
// add destroy method that stops the timer and removes itself from the list
timer.destroy = function () {
timer.stop();
delete _timers[cid];
};
timer.start();
}
// make sure to stop and delete the attached timer when the computation is stopped
comp.onInvalidate(() => {
// console.log('onInvalidated',comp);
if (comp.stopped && _timers[cid]) {
// console.log('computation stopped');
_timers[cid].destroy();
}
});
_timers[cid].time.dep.depend(comp); // make dependent on time
// console.log(_timers);
return _timers[cid];
}
// reactive version of moment()
// please install moment separately
// example usage: Chronos.moment(someTimestamp).format('ss');
function _moment(/* arguments */) {
if (!moment) throw new Error('moment not found. Please install it first');
_update();
return moment(...arguments);
}
// reactive version of new Date() get the current date/time
function _date(interval) {
_update(interval);
return new Date();
}
// reactive version of Date.now(). get the current # of milliseconds since the start of the epoch
function _now(interval) {
_update(interval);
return Date.now();
}
// export global
const Chronos = {
// a simple reactive timer
// usage: var timer = new Timer();
// get current time: timer.time.get();
Timer,
// handy util func for making reactive contexts live updating in time
// usage: simply call Chronos.update() in your helper to make it execute
// every interval
update: _update,
// reactive version of new Date() get the current date/time
date: _date,
// reactive version of Date.now(). get the current # of milliseconds since the start of the epoch
now: _now,
// reactive version of moment()
// please install moment separately
// example usage: Chronos.moment(someTimestamp).format('ss');
moment: _moment,
// for debugging and testing
_timers,
// deprecated. but kept for backwards compatibility
liveUpdate: _update,
currentTime: _date,
liveMoment: _moment
};
export default Chronos;

View file

@ -0,0 +1,52 @@
/* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import Chronos from '/imports/ui/components/Chronos/Chronos';
class FromNow extends Component {
constructor(props) {
super(props);
this.state = {
when: props.when
};
}
componentWillReceiveProps(nextProps) {
if (this.props.when !== nextProps.when) {
// console.log(`Next when ${nextProps.when}`);
this.setState({
when: nextProps.when
});
}
}
shouldComponentUpdate(nextProps, nextState) {
return !(nextState.when === this.state.when);
}
render() {
console.log(`Render from now ${this.state.when}`);
return (
<span>{this.state.when}</span>
);
}
}
FromNow.propTypes = {
when: PropTypes.string
};
FromNow.defaultProps = {
};
const FromNowContainer = withTracker((props) => {
const whenDate = props.when;
return {
when: whenDate ? Chronos.moment(whenDate).fromNow() : null
};
})(FromNow);
export default FromNowContainer;

View file

@ -1,4 +1,3 @@
/* global Chronos */
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */ /* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */ /* eslint-disable react/jsx-indent */
@ -6,13 +5,14 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { translate } from 'react-i18next'; import { translate, Trans } from 'react-i18next';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Map, Circle } from 'react-leaflet'; import { Map, Circle } from 'react-leaflet';
import Blaze from 'meteor/gadicc:blaze-react-component'; import Blaze from 'meteor/gadicc:blaze-react-component';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import NotFound from '/imports/ui/pages/NotFound/NotFound'; import NotFound from '/imports/ui/pages/NotFound/NotFound';
import FiresCollection from '/imports/api/Fires/Fires'; import FiresCollection from '/imports/api/Fires/Fires';
import FromNow from '/imports/ui/components/FromNow/FromNow';
import { dateLongFormat } from '/imports/api/Common/dates'; import { dateLongFormat } from '/imports/api/Common/dates';
import '/imports/startup/client/comments'; import '/imports/startup/client/comments';
import './Fires.scss'; import './Fires.scss';
@ -40,10 +40,9 @@ class Fire extends React.Component {
render() { render() {
const { loading, fire, t } = this.props; const { loading, fire, t } = this.props;
console.log(`Render ${this.state.when}`);
if (fire && fire.when) { if (fire && fire.when) {
this.dateLongFormat = dateLongFormat(fire.when); this.dateLongFormat = dateLongFormat(fire.when);
this.dateFromNow = this.state.when; // this.dateFromNow = () => (<FromNow {...this.props} />);
} }
return (fire ? return (fire ?
(<div className="ViewFire"> (<div className="ViewFire">
@ -55,50 +54,50 @@ class Fire extends React.Component {
t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat })} t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat })}
</h4> </h4>
<Map <Map
ref={(map) => { ref={(map) => {
this.fireMap = map; this.fireMap = map;
}} }}
animate animate
sleep={false} sleep={false}
center={[fire.lat, fire.lon]} center={[fire.lat, fire.lon]}
className="fire-leaflet-container" className="fire-leaflet-container"
zoom={12} zoom={12}
> >
<Fragment> <Fragment>
<Circle <Circle
center={[fire.lat, fire.lon]} center={[fire.lat, fire.lon]}
color="red" color="red"
fillColor="red" fillColor="red"
fillOpacity={0.1} fillOpacity={0.1}
radius={fire.scan ? fire.scan * 1000 : 300} radius={fire.scan ? fire.scan * 1000 : 300}
/> />
</Fragment> </Fragment>
<DefMapLayers /> <DefMapLayers />
</Map> </Map>
<p>{t('Coordenadas:')} {fire.lat}, {fire.lon}</p> <p>{t('Coordenadas:')} {fire.lat}, {fire.lon}</p>
{(fire.type === 'modis' || fire.type === 'viirs') && {(fire.type === 'modis' || fire.type === 'viirs') &&
<p>{t('Fuego detectado por satélites de la NASA {{when}}', { when: this.dateFromNow })}</p> <p><Trans>Fuego detectado por satélites de la NASA <FromNow {...this.props} /></Trans></p>
}
{(fire.type === 'vecinal') &&
<p><Trans>Fuego notificado por uno de nuestros usuarios/as <FromNow {...this.props} /></Trans></p>
} }
{(fire.type === 'vecinal') && {/* TODO: marcar tipo de fuego, industria, etc */}
<p>{t('Fuego notificado por uno de nuestros usuarios/as {{when}}', { when: this.dateFromNow })}</p> <h4>{t('Comentarios')}</h4>
} <div className="comments-info">
{/* TODO: marcar tipo de fuego, industria, etc */} {t('Puedes añadir un comentario si tienes información adicional sobre este fuego.')}
<h4>{t('Comentarios')}</h4> {' '}
<div className="comments-info"> {t('Por ejemplo:')}
{t('Puedes añadir un comentario si tienes información adicional sobre este fuego.')} <ul>
{' '} <li>{t('si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)')}</li>
{t('Por ejemplo:')} <li>{t('si conoces el motivo por el que comenzó el fuego')}</li>
<ul> <li>{t('si quieres denunciar algún tipo de ilegalidad, incluso anónimamente')}</li>
<li>{t('si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)')}</li> <li>{t('o cualquier otra información')}</li>
<li>{t('si conoces el motivo por el que comenzó el fuego')}</li> </ul>
<li>{t('si quieres denunciar algún tipo de ilegalidad, incluso anónimamente')}</li> </div>
<li>{t('o cualquier otra información')}</li> <div className="comments-section">
</ul> <Blaze template="commentsBox" id={`fire-${fire._id}`} />
</div> </div>
<div className="comments-section">
<Blaze template="commentsBox" id={`fire-${fire._id}`} />
</div>
</Fragment> </Fragment>
} }
</div> </div>
@ -109,7 +108,7 @@ class Fire extends React.Component {
Fire.propTypes = { Fire.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
when: PropTypes.string, when: PropTypes.instanceOf(Date),
fire: PropTypes.object // .isRequired fire: PropTypes.object // .isRequired
}; };
@ -118,16 +117,14 @@ Fire.defaultProps = {
// export default translate([], { wait: true })(withTracker((props) => { // export default translate([], { wait: true })(withTracker((props) => {
const FireContainer = withTracker(({ match }) => { const FireContainer = withTracker(({ match, i18n }) => {
const fireEncrypt = match.params.id; const fireEncrypt = match.params.id;
const subscription = Meteor.subscribe('fireFromHash', fireEncrypt); const subscription = Meteor.subscribe('fireFromHash', fireEncrypt);
// });
// console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`); // console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`);
return { return {
loading: !subscription.ready(), loading: !subscription.ready(),
fire: FiresCollection.findOne(), fire: FiresCollection.findOne(),
when: subscription.ready() ? Chronos.moment(FiresCollection.findOne().when).fromNow() : null when: subscription.ready() ? FiresCollection.findOne().when : null
}; };
})(Fire); })(Fire);

View file

@ -21,9 +21,11 @@ import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/Center
import FireList from '/imports/ui/components/Maps/FireList'; import FireList from '/imports/ui/components/Maps/FireList';
import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion'; import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import FromNow from '/imports/ui/components/FromNow/FromNow';
import Loading from '/imports/ui/components/Loading/Loading'; import Loading from '/imports/ui/components/Loading/Loading';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts'; import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
import { isNotHomeAndMobile, isChrome } from '/imports/ui/components/Utils/isMobile'; import { isNotHomeAndMobile, isChrome } from '/imports/ui/components/Utils/isMobile';
@ -140,8 +142,8 @@ class FiresMap extends React.Component {
<Col xs={12} sm={6} md={6} lg={6} > <Col xs={12} sm={6} md={6} lg={6} >
<p className="firesmap-legend"> <p className="firesmap-legend">
{ this.props.activefires.length === 0 ? { this.props.activefires.length === 0 ?
<Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. Hay un total de <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos detectados en todo el mundo.</Trans> : <Fragment><Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. Hay un total de <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos detectados en todo el mundo.</Trans> <Trans>Datos actualizados <FromNow when={this.props.lastCheck} /></Trans></Fragment> :
<Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefires.length }}</strong> fuegos activos en el mapa. Hay un total de <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos detectados en todo el mundo por la NASA.</Trans> <Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefires.length }}</strong> fuegos activos en el mapa. Hay un total de <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos detectados en todo el mundo por la NASA.</Trans> <Trans>Datos actualizados <FromNow when={this.props.lastCheck} /></Trans></Fragment>
} }
</p> </p>
{isNotHomeAndMobile && {isNotHomeAndMobile &&
@ -228,6 +230,7 @@ FiresMap.propTypes = {
userSubs: PropTypes.arrayOf(PropTypes.object).isRequired, userSubs: PropTypes.arrayOf(PropTypes.object).isRequired,
activefires: PropTypes.arrayOf(PropTypes.object).isRequired, activefires: PropTypes.arrayOf(PropTypes.object).isRequired,
firealerts: PropTypes.arrayOf(PropTypes.object).isRequired, firealerts: PropTypes.arrayOf(PropTypes.object).isRequired,
lastCheck: PropTypes.instanceOf(Date),
activefirestotal: PropTypes.number.isRequired, activefirestotal: PropTypes.number.isRequired,
center: PropTypes.arrayOf(PropTypes.number), center: PropTypes.arrayOf(PropTypes.number),
zoom: PropTypes.number, zoom: PropTypes.number,
@ -258,12 +261,14 @@ export default translate([], { wait: true })(withTracker(() => {
Meteor.subscribe('activefirestotal'); Meteor.subscribe('activefirestotal');
// Right now to all neighborhood alerts // Right now to all neighborhood alerts
Meteor.subscribe('fireAlerts'); Meteor.subscribe('fireAlerts');
Meteor.subscribe('settings');
const userSubs = Meteor.subscribe('userSubsToFires'); const userSubs = Meteor.subscribe('userSubsToFires');
// const subscription = Meteor.subscribe('activefiresmyloc', zoom.get()); // const subscription = Meteor.subscribe('activefiresmyloc', zoom.get());
// Warning with the performance of this log: // Warning with the performance of this log:
// console.log(`Active fires ${ActiveFiresCollection.find().count()} of ${Counter.get('countActiveFires')}`); // console.log(`Active fires ${ActiveFiresCollection.find().count()} of ${Counter.get('countActiveFires')}`);
// console.log(`Active neighborhood fires ${FireAlertsCollection.find().fetch().length} and users subscribed ${UserSubsToFiresCollection.find().fetch().length}`); // console.log(`Active neighborhood fires ${FireAlertsCollection.find().fetch().length} and users subscribed ${UserSubsToFiresCollection.find().fetch().length}`);
// console.log(UserSubsToFiresCollection.find().fetch()); // console.log(UserSubsToFiresCollection.find().fetch());
const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' });
return { return {
loading: !subscription.ready(), loading: !subscription.ready(),
userSubs: UserSubsToFiresCollection.find().fetch(), userSubs: UserSubsToFiresCollection.find().fetch(),
@ -273,6 +278,7 @@ export default translate([], { wait: true })(withTracker(() => {
// activefires: ActiveFiresCollection.find({}).fetch(), // activefires: ActiveFiresCollection.find({}).fetch(),
activefirestotal: Counter.get('countActiveFires'), activefirestotal: Counter.get('countActiveFires'),
firealerts: FireAlertsCollection.find().fetch(), firealerts: FireAlertsCollection.find().fetch(),
lastCheck: lastCheck ? lastCheck.value : null,
center: geolocation.get(), center: geolocation.get(),
zoom: zoom.get() zoom: zoom.get()
}; };

View file

@ -142,7 +142,7 @@
"Información adicional sobre fuego detectado en {{where}} el {{when}}": "Additional information about fire detected in {{where}} on {{when}}", "Información adicional sobre fuego detectado en {{where}} el {{when}}": "Additional information about fire detected in {{where}} on {{when}}",
"Información adicional sobre fuego detectado el {{when}}": "Additional information about fire detected on {{when}}", "Información adicional sobre fuego detectado el {{when}}": "Additional information about fire detected on {{when}}",
"Coordenadas:": "Coordinates:", "Coordenadas:": "Coordinates:",
"Fuego detectado por satélites de la NASA {{when}}": "Fire detected by NASA satellites {{when}}", "Fuego detectado por satélites de la NASA <1></1>": "Fire detected by NASA satellites <1></1>",
"Puedes añadir un comentario si tienes información adicional sobre este fuego.": "You can add a comment if you have additional information about this fire.", "Puedes añadir un comentario si tienes información adicional sobre este fuego.": "You can add a comment if you have additional information about this fire.",
"Por ejemplo:": "For example:", "Por ejemplo:": "For example:",
"si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)": "If you know the area and how to access the fire (this can be helpful to turn off if still active or to investigate in the future)", "si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)": "If you know the area and how to access the fire (this can be helpful to turn off if still active or to investigate in the future)",
@ -152,7 +152,7 @@
"Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.": "Zoom into an area of your interest if you want that fires to be updated in real time.", "Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.": "Zoom into an area of your interest if you want that fires to be updated in real time.",
"Notificaciones": "Notifications", "Notificaciones": "Notifications",
"Recibe nuestras notificaciones de fuegos por correo o en tu navegador": "Receive our fire notifications by mail or in your browser", "Recibe nuestras notificaciones de fuegos por correo o en tu navegador": "Receive our fire notifications by mail or in your browser",
"Fuego notificado por uno de nuestros usuarios/as {{when}}": "Fire reported by one of our users {{when}}", "Fuego notificado por uno de nuestros usuarios/as <1></1>": "Fire reported by one of our users <1></1>",
"No recibirás notificaciones de fuegos en este equipo, solo por correo": "You will not receive notifications of fires in this device, only by email", "No recibirás notificaciones de fuegos en este equipo, solo por correo": "You will not receive notifications of fires in this device, only by email",
"not-found": "Oops: This page doesn't exist", "not-found": "Oops: This page doesn't exist",
"Más información sobre este fuego": "More information about this fire", "Más información sobre este fuego": "More information about this fire",
@ -160,5 +160,6 @@
"Bienvenid@!": "Welcome!", "Bienvenid@!": "Welcome!",
"Verifica tu dirección de correo": "Verify Your Email Address", "Verifica tu dirección de correo": "Verify Your Email Address",
"Zonas vigiladas": "Monitored areas", "Zonas vigiladas": "Monitored areas",
"En verde, las zonas vigiladas por nuestros usuari@s actualmente": "In green, the areas monitored by our users currently" "En verde, las zonas vigiladas por nuestros usuari@s actualmente": "In green, the areas monitored by our users currently",
"Datos actualizados <1></1>": "Data updated <1></1>"
} }

View file

@ -210,7 +210,7 @@
"Información adicional sobre fuego detectado el {{when}}": "Información adicional sobre fuego detectado el {{when}}", "Información adicional sobre fuego detectado el {{when}}": "Información adicional sobre fuego detectado el {{when}}",
"Coordenadas:": "Coordenadas:", "Coordenadas:": "Coordenadas:",
"Fuego detectado por satélites de la NASA {{when}}": "Fuego detectado por satélites de la NASA {{when}}", "Fuego detectado por satélites de la NASA <1></1>": "Fuego detectado por satélites de la NASA <1></1>",
"Puedes añadir un comentario si tienes información adicional sobre este fuego.": "Puedes añadir un comentario si tienes información adicional sobre este fuego.", "Puedes añadir un comentario si tienes información adicional sobre este fuego.": "Puedes añadir un comentario si tienes información adicional sobre este fuego.",
"Por ejemplo:": "Por ejemplo:", "Por ejemplo:": "Por ejemplo:",
"si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)": "si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)", "si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)": "si conoces esta zona y cómo acceder al fuego (esto puede de ser de ayuda para apagarlo si sigue activo o para investigarlo en un futuro)",
@ -220,7 +220,7 @@
"Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.": "Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.", "Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.": "Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.",
"Notificaciones": "Notificaciones", "Notificaciones": "Notificaciones",
"Recibe nuestras notificaciones de fuegos por correo o en tu navegador": "Recibe nuestras notificaciones de fuegos por correo o en tu navegador", "Recibe nuestras notificaciones de fuegos por correo o en tu navegador": "Recibe nuestras notificaciones de fuegos por correo o en tu navegador",
"Fuego notificado por uno de nuestros usuarios/as {{when}}": "Fuego notificado por uno de nuestros usuarios/as {{when}}", "Fuego notificado por uno de nuestros usuarios/as <1></1>": "Fuego notificado por uno de nuestros usuarios/as <1></1>",
"No recibirás notificaciones de fuegos en este equipo, solo por correo": "No recibirás notificaciones de fuegos en este equipo, solo por correo", "No recibirás notificaciones de fuegos en este equipo, solo por correo": "No recibirás notificaciones de fuegos en este equipo, solo por correo",
"not-found": "Upppps: Esta página no existe", "not-found": "Upppps: Esta página no existe",
"Más información sobre este fuego": "Más información sobre este fuego", "Más información sobre este fuego": "Más información sobre este fuego",
@ -231,5 +231,6 @@
"Verifica tu dirección de correo": "Verifica tu dirección de correo":
"Verifica tu dirección de correo", "Verifica tu dirección de correo",
"Zonas vigiladas": "Zonas vigiladas", "Zonas vigiladas": "Zonas vigiladas",
"En verde, las zonas vigiladas por nuestros usuari@s actualmente": "En verde, las zonas vigiladas por nuestros usuari@s actualmente" "En verde, las zonas vigiladas por nuestros usuari@s actualmente": "En verde, las zonas vigiladas por nuestros usuari@s actualmente",
"Datos actualizados <1></1>": "Datos actualizados <1></1>"
} }

49
test/siteSettings.test.js Normal file
View file

@ -0,0 +1,49 @@
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
/* eslint-disable import/no-absolute-path */
import { chai } from 'meteor/practicalmeteor:chai';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import SiteSettingsTypes from '/imports/api/SiteSettings/SiteSettingsTypes';
const setting = {
name: 'site-test',
value: 'Some value',
description: 'Some description',
type: 'string'
};
describe('site settings store', () => {
it('should get settingstypes', () => {
chai.expect(SiteSettingsTypes.string.value.type).equal(String);
});
it('should insert settings', () => {
const id = SiteSettings.insert(setting);
SiteSettings.getSchema(setting.type).validate(setting);
const inserted = SiteSettings.findOne(id);
delete inserted._id;
chai.expect(setting).to.deep.equal(inserted);
SiteSettings.remove(id);
chai.expect(SiteSettings.find({ _id: inserted }).count()).equal(0);
});
it('should not insert twice', () => {
const id = SiteSettings.insert(setting);
// If this fails manual check that the setting it's not already in the db.
chai.expect(() => {
SiteSettings.insert(setting);
}).to.throw('E11000 duplicate key error collection: fuegos.siteSettings index: name_1 dup key: { : "site-test" }');
const inserted = SiteSettings.find(id).count();
chai.expect(inserted).equal(1);
SiteSettings.remove(id);
chai.expect(SiteSettings.find({ _id: inserted }).count()).equal(0);
});
it('should fail validation', () => {
chai.expect(() => {
SiteSettings.getSchema('boolean').validate(setting);
}).to.throw('Value must be of type Boolean');
});
});