From 379e72a9e79f724721993824e16ece2f2346177a Mon Sep 17 00:00:00 2001 From: vjrj Date: Mon, 29 Jan 2018 11:30:17 +0100 Subject: [PATCH] Site settings and fires fromNow Reactive --- .meteor/packages | 1 - .meteor/versions | 2 - imports/api/SiteSettings/SiteSettings.js | 49 +++++++ imports/api/SiteSettings/SiteSettingsTypes.js | 66 ++++++++++ imports/api/SiteSettings/methods.js | 60 +++++++++ .../api/SiteSettings/server/publications.js | 6 + imports/startup/server/api.js | 3 + imports/startup/server/migrations.js | 11 +- imports/ui/components/Chronos/Chronos.js | 122 ++++++++++++++++++ imports/ui/components/FromNow/FromNow.js | 52 ++++++++ imports/ui/pages/Fires/Fires.js | 101 +++++++-------- imports/ui/pages/FiresMap/FiresMap.js | 10 +- public/locales/en/common.json | 7 +- public/locales/es/common.json | 7 +- test/siteSettings.test.js | 49 +++++++ 15 files changed, 482 insertions(+), 64 deletions(-) create mode 100644 imports/api/SiteSettings/SiteSettings.js create mode 100644 imports/api/SiteSettings/SiteSettingsTypes.js create mode 100644 imports/api/SiteSettings/methods.js create mode 100644 imports/api/SiteSettings/server/publications.js create mode 100644 imports/ui/components/Chronos/Chronos.js create mode 100644 imports/ui/components/FromNow/FromNow.js create mode 100644 test/siteSettings.test.js diff --git a/.meteor/packages b/.meteor/packages index 10ae5fc..ee82606 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -60,4 +60,3 @@ mongo-livedata reywood:publish-composite barbatus:stars-rating arkham:comments-ui -remcoder:chronos diff --git a/.meteor/versions b/.meteor/versions index adc9c37..8a03c20 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -111,7 +111,6 @@ react-meteor-data@0.2.15 reactive-dict@1.2.0 reactive-var@1.0.11 reload@1.1.11 -remcoder:chronos@0.5.0 retry@1.0.9 reywood:publish-composite@1.5.2 routepolicy@1.0.12 @@ -138,4 +137,3 @@ url@1.1.0 vjrj:piwik@0.3.1 webapp@1.4.0 webapp-hashing@1.0.9 -zodiase:function-bind@0.0.1 diff --git a/imports/api/SiteSettings/SiteSettings.js b/imports/api/SiteSettings/SiteSettings.js new file mode 100644 index 0000000..e690671 --- /dev/null +++ b/imports/api/SiteSettings/SiteSettings.js @@ -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; diff --git a/imports/api/SiteSettings/SiteSettingsTypes.js b/imports/api/SiteSettings/SiteSettingsTypes.js new file mode 100644 index 0000000..bff3c00 --- /dev/null +++ b/imports/api/SiteSettings/SiteSettingsTypes.js @@ -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; diff --git a/imports/api/SiteSettings/methods.js b/imports/api/SiteSettings/methods.js new file mode 100644 index 0000000..737bc3a --- /dev/null +++ b/imports/api/SiteSettings/methods.js @@ -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 +}); diff --git a/imports/api/SiteSettings/server/publications.js b/imports/api/SiteSettings/server/publications.js new file mode 100644 index 0000000..cd7f33d --- /dev/null +++ b/imports/api/SiteSettings/server/publications.js @@ -0,0 +1,6 @@ +/* eslint-disable prefer-arrow-callback */ + +import { Meteor } from 'meteor/meteor'; +import SiteSettings from '../SiteSettings'; + +Meteor.publish('settings', () => SiteSettings.find()); diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index bfb830e..64b3e9f 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -21,3 +21,6 @@ import '../../api/Notifications/server/publications'; import '../../api/Fires/methods'; import '../../api/Fires/server/publications'; + +import '../../api/SiteSettings/methods'; +import '../../api/SiteSettings/server/publications'; diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index d8ecff4..1c49a4c 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base'; import randomHex from 'crypto-random-hex'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts'; +import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; Meteor.startup(() => { // 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 Migrations.migrateTo('latest'); - // Migrations.migrateTo('2,rerun'); + // Migrations.migrateTo('5,rerun'); }); diff --git a/imports/ui/components/Chronos/Chronos.js b/imports/ui/components/Chronos/Chronos.js new file mode 100644 index 0000000..6a96c31 --- /dev/null +++ b/imports/ui/components/Chronos/Chronos.js @@ -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; diff --git a/imports/ui/components/FromNow/FromNow.js b/imports/ui/components/FromNow/FromNow.js new file mode 100644 index 0000000..b540097 --- /dev/null +++ b/imports/ui/components/FromNow/FromNow.js @@ -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 ( + {this.state.when} + ); + } +} + +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; diff --git a/imports/ui/pages/Fires/Fires.js b/imports/ui/pages/Fires/Fires.js index 3d45204..38c7aad 100644 --- a/imports/ui/pages/Fires/Fires.js +++ b/imports/ui/pages/Fires/Fires.js @@ -1,4 +1,3 @@ -/* global Chronos */ /* eslint-disable import/no-absolute-path */ /* eslint-disable react/jsx-indent-props */ /* eslint-disable react/jsx-indent */ @@ -6,13 +5,14 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { withTracker } from 'meteor/react-meteor-data'; -import { translate } from 'react-i18next'; +import { translate, Trans } from 'react-i18next'; import { Meteor } from 'meteor/meteor'; import { Map, Circle } from 'react-leaflet'; import Blaze from 'meteor/gadicc:blaze-react-component'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import NotFound from '/imports/ui/pages/NotFound/NotFound'; import FiresCollection from '/imports/api/Fires/Fires'; +import FromNow from '/imports/ui/components/FromNow/FromNow'; import { dateLongFormat } from '/imports/api/Common/dates'; import '/imports/startup/client/comments'; import './Fires.scss'; @@ -40,10 +40,9 @@ class Fire extends React.Component { render() { const { loading, fire, t } = this.props; - console.log(`Render ${this.state.when}`); if (fire && fire.when) { this.dateLongFormat = dateLongFormat(fire.when); - this.dateFromNow = this.state.when; + // this.dateFromNow = () => (); } return (fire ? (
@@ -55,50 +54,50 @@ class Fire extends React.Component { t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat })} - { - this.fireMap = map; - }} - animate - sleep={false} - center={[fire.lat, fire.lon]} - className="fire-leaflet-container" - zoom={12} - > - - - - - -

{t('Coordenadas:')} {fire.lat}, {fire.lon}

- {(fire.type === 'modis' || fire.type === 'viirs') && -

{t('Fuego detectado por satélites de la NASA {{when}}', { when: this.dateFromNow })}

+ { + this.fireMap = map; + }} + animate + sleep={false} + center={[fire.lat, fire.lon]} + className="fire-leaflet-container" + zoom={12} + > + + + + + +

{t('Coordenadas:')} {fire.lat}, {fire.lon}

+ {(fire.type === 'modis' || fire.type === 'viirs') && +

Fuego detectado por satélites de la NASA

+ } + {(fire.type === 'vecinal') && +

Fuego notificado por uno de nuestros usuarios/as

} - {(fire.type === 'vecinal') && -

{t('Fuego notificado por uno de nuestros usuarios/as {{when}}', { when: this.dateFromNow })}

- } - {/* TODO: marcar tipo de fuego, industria, etc */} -

{t('Comentarios')}

-
- {t('Puedes añadir un comentario si tienes información adicional sobre este fuego.')} - {' '} - {t('Por ejemplo:')} -
    -
  • {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)')}
  • -
  • {t('si conoces el motivo por el que comenzó el fuego')}
  • -
  • {t('si quieres denunciar algún tipo de ilegalidad, incluso anónimamente')}
  • -
  • {t('o cualquier otra información')}
  • -
-
-
- -
+ {/* TODO: marcar tipo de fuego, industria, etc */} +

{t('Comentarios')}

+
+ {t('Puedes añadir un comentario si tienes información adicional sobre este fuego.')} + {' '} + {t('Por ejemplo:')} +
    +
  • {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)')}
  • +
  • {t('si conoces el motivo por el que comenzó el fuego')}
  • +
  • {t('si quieres denunciar algún tipo de ilegalidad, incluso anónimamente')}
  • +
  • {t('o cualquier otra información')}
  • +
+
+
+ +
}
@@ -109,7 +108,7 @@ class Fire extends React.Component { Fire.propTypes = { t: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, - when: PropTypes.string, + when: PropTypes.instanceOf(Date), fire: PropTypes.object // .isRequired }; @@ -118,16 +117,14 @@ Fire.defaultProps = { // export default translate([], { wait: true })(withTracker((props) => { -const FireContainer = withTracker(({ match }) => { +const FireContainer = withTracker(({ match, i18n }) => { const fireEncrypt = match.params.id; const subscription = Meteor.subscribe('fireFromHash', fireEncrypt); - - // }); // console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`); return { loading: !subscription.ready(), fire: FiresCollection.findOne(), - when: subscription.ready() ? Chronos.moment(FiresCollection.findOne().when).fromNow() : null + when: subscription.ready() ? FiresCollection.findOne().when : null }; })(Fire); diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index 032455d..21cf30d 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -21,9 +21,11 @@ import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/Center import FireList from '/imports/ui/components/Maps/FireList'; import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion'; 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 ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts'; +import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; import { isNotHomeAndMobile, isChrome } from '/imports/ui/components/Utils/isMobile'; @@ -140,8 +142,8 @@ class FiresMap extends React.Component {

{ this.props.activefires.length === 0 ? - No hay fuegos activos en esta zona del mapa. Hay un total de {{ countTotal: this.props.activefirestotal }} fuegos activos detectados en todo el mundo. : - En rojo, {{ count: this.props.activefires.length }} fuegos activos en el mapa. Hay un total de {{ countTotal: this.props.activefirestotal }} fuegos activos detectados en todo el mundo por la NASA. + No hay fuegos activos en esta zona del mapa. Hay un total de {{ countTotal: this.props.activefirestotal }} fuegos activos detectados en todo el mundo. Datos actualizados : + En rojo, {{ count: this.props.activefires.length }} fuegos activos en el mapa. Hay un total de {{ countTotal: this.props.activefirestotal }} fuegos activos detectados en todo el mundo por la NASA. Datos actualizados }

{isNotHomeAndMobile && @@ -228,6 +230,7 @@ FiresMap.propTypes = { userSubs: PropTypes.arrayOf(PropTypes.object).isRequired, activefires: PropTypes.arrayOf(PropTypes.object).isRequired, firealerts: PropTypes.arrayOf(PropTypes.object).isRequired, + lastCheck: PropTypes.instanceOf(Date), activefirestotal: PropTypes.number.isRequired, center: PropTypes.arrayOf(PropTypes.number), zoom: PropTypes.number, @@ -258,12 +261,14 @@ export default translate([], { wait: true })(withTracker(() => { Meteor.subscribe('activefirestotal'); // Right now to all neighborhood alerts Meteor.subscribe('fireAlerts'); + Meteor.subscribe('settings'); const userSubs = Meteor.subscribe('userSubsToFires'); // const subscription = Meteor.subscribe('activefiresmyloc', zoom.get()); // Warning with the performance of this log: // 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(UserSubsToFiresCollection.find().fetch()); + const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' }); return { loading: !subscription.ready(), userSubs: UserSubsToFiresCollection.find().fetch(), @@ -273,6 +278,7 @@ export default translate([], { wait: true })(withTracker(() => { // activefires: ActiveFiresCollection.find({}).fetch(), activefirestotal: Counter.get('countActiveFires'), firealerts: FireAlertsCollection.find().fetch(), + lastCheck: lastCheck ? lastCheck.value : null, center: geolocation.get(), zoom: zoom.get() }; diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 844c7b7..4abdaa9 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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 el {{when}}": "Additional information about fire detected on {{when}}", "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>": "Fire detected by NASA satellites <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.", "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)", @@ -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.", "Notificaciones": "Notifications", "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>": "Fire reported by one of our users <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", "not-found": "Oops: This page doesn't exist", "Más información sobre este fuego": "More information about this fire", @@ -160,5 +160,6 @@ "Bienvenid@!": "Welcome!", "Verifica tu dirección de correo": "Verify Your Email Address", "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>": "Data updated <1>" } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index cad80d6..b96bf67 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -210,7 +210,7 @@ "Información adicional sobre fuego detectado el {{when}}": "Información adicional sobre fuego detectado el {{when}}", "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>": "Fuego detectado por satélites de la NASA <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.", "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)", @@ -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.", "Notificaciones": "Notificaciones", "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>": "Fuego notificado por uno de nuestros usuarios/as <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", "not-found": "Upppps: Esta página no existe", "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", "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>": "Datos actualizados <1>" } diff --git a/test/siteSettings.test.js b/test/siteSettings.test.js new file mode 100644 index 0000000..a2b3888 --- /dev/null +++ b/test/siteSettings.test.js @@ -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'); + }); +});