Compare commits

..

61 commits

Author SHA1 Message Date
39415808bf Fase 1a: flag de cutover notifDisabledChannels en processNotif
Guard reversible al inicio de processNotif: los canales listados en
settings.private.notifDisabledChannels (p.ej. ['mobile','web']) los gestiona el
microservicio tcef-notifications, y el observer/cron viejos los ignoran.
Exclusion mutua por canal sin desplegar codigo (solo editar settings).
ES5-compatible (Meteor 1.6 / Node 8).
2026-07-13 00:10:39 +02:00
9d92ec4f88 deps: regenera package-lock.json y actualiza puntero de meteor-comments-ui 2026-07-12 23:26:17 +02:00
2a31435135 Refactor server startup: elimina segfaults, añade leaflet-workaround
Retira import './segfaults' (workaround de crash obsoleto) y añade
leaflet-workaround.js (shims global window/document/navigator para
render de Leaflet en servidor). Ajustes menores en publications de
FalsePositives y subsUnion.
2026-07-12 23:26:16 +02:00
8281fa3427 chore: ignorar settings-production-*.json y settings-tests.json (secretos) 2026-07-12 23:26:16 +02:00
vjrj
ee7af168d6 Update meteor dev user/pass 2022-08-14 21:53:41 +02:00
vjrj
4cbbe65fa3 Fix wrong grey tiles 2022-08-13 19:37:56 +02:00
vjrj
b9457fe141 Minor changes in fires union 2018-12-23 13:31:41 +01:00
vjrj
c4cb52abaf Improved chimp command in README 2018-11-18 18:01:38 +01:00
vjrj
423dbc5ed9 Added optional chimp configuration 2018-11-18 18:01:17 +01:00
vjrj
1fdf86dd47 Union of subs 'null' when app is initialized 2018-11-18 16:24:49 +01:00
vjrj
5f9a84dfd7 Unify don't calc boundaries of empty union 2018-11-18 15:45:14 +01:00
vjrj
564b9e8ead Trying to fix race condition in i18n load 2018-11-18 15:43:48 +01:00
vjrj
df51368d50 Better log in error 2018-11-18 15:05:04 +01:00
vjrj
0f6dd21541 Removed watch to test 2018-11-18 15:04:46 +01:00
vjrj
8903c765e3 Minor refactor 2018-11-18 14:27:51 +01:00
vjrj
266707052c Better error message 2018-11-18 14:27:34 +01:00
vjrj
3f8bc3666b Fixed index DOM warning. Google Play btn height 2018-11-18 13:08:36 +01:00
vjrj
6abe668740 Added fires union (wip) 2018-11-18 11:48:03 +01:00
vjrj
7b5f17fbe8 Added activeFiresUnion (wip) 2018-10-28 20:27:38 +01:00
vjrj
8b598fab7c Disabled fires render 2018-10-15 06:02:05 +02:00
vjrj
cb7aeda4b8 Create a sender on each message 2018-09-20 15:21:38 +02:00
vjrj
181f488263 New map libs 2018-09-20 15:21:17 +02:00
vjrj
e96e6ee8f7 Union of polygon to lib 2018-09-16 17:06:23 +02:00
vjrj
df47bef794 Fix API now using track 2018-09-15 10:27:03 +02:00
vjrj
3ea653552e Removed typo 2018-09-13 10:13:47 +02:00
vjrj
6063437df0 Remove email notifications from not validated emails 2018-09-13 10:09:09 +02:00
vjrj
cdc3bbdd5b Notif stats to cron process 2018-09-13 10:08:26 +02:00
vjrj
33c68001c2 _str ignore 2018-09-13 10:08:13 +02:00
vjrj
53a7c3be21 Process also pending emailNotifications 2018-09-11 15:27:10 +02:00
vjrj
3d059287ae Notif process also in cron 2018-09-11 14:42:31 +02:00
vjrj
a208eb7ea1 More work with unify, circle/square 2018-09-11 11:40:17 +02:00
vjrj
c4b2f8cd73 Update README with FAQ 2018-08-28 13:15:56 +02:00
vjrj
f9febb4415 Added fire type to REST call 2018-08-22 08:10:35 +02:00
Vicente J. Ruiz Jurado
f5665a56a8 Hardcode available list of langs in profile 2018-08-17 11:33:29 +02:00
Vicente J. Ruiz Jurado
4d7938220b Added comment 2018-08-17 06:13:57 +02:00
Vicente J. Ruiz Jurado
829fae4f07 Trying to get available list of langs in profile 2018-08-17 05:39:03 +02:00
Vicente J. Ruiz Jurado
e223ddb643 Preload langs 2018-08-16 22:44:38 +02:00
Vicente J. Ruiz Jurado
dc5e38f8d5 gl fallback to es in T9 2018-08-16 19:04:46 +02:00
Vicente J. Ruiz Jurado
ed08563cad Thanks for translation 2018-08-16 17:48:58 +02:00
Vicente J. Ruiz Jurado
9a31ab2457 Commenting gl in t9 accounts 2018-08-16 17:47:00 +02:00
Vicente J. Ruiz Jurado
f608fc4d28 Enabling Galego in interface 2018-08-16 17:32:45 +02:00
Weblate
6f683ebaa2 Merge remote-tracking branch 'origin/tcef-master' into tcef-master 2018-08-16 09:03:55 +00:00
lupe bao reixa
8e729a6939 Translated using Weblate (Galician)
Currently translated at 100.0% (221 of 221 strings)
2018-08-16 09:03:51 +00:00
Vicente J. Ruiz Jurado
ffef42d19a Added play button 2018-08-15 07:26:39 +02:00
Vicente J. Ruiz Jurado
3defbb01a2 Fix for false positive rest call 2018-08-15 06:18:51 +02:00
Vicente J. Ruiz Jurado
bbdc53bda1 More logs in Rest 2018-08-11 08:15:04 +02:00
Vicente J. Ruiz Jurado
8a741dc729 Adding log to false positive 2018-08-10 11:12:33 +02:00
Vicente J. Ruiz Jurado
15ed7e917a Adding log to false positive 2018-08-10 09:07:05 +02:00
Vicente J. Ruiz Jurado
759797e412 Fix for rest method for false positive stats when no fires 2018-08-08 08:42:52 +02:00
Vicente J. Ruiz Jurado
5e6271bb88 Trying to fix fireToHash func export 2018-08-05 10:58:35 +02:00
Vicente J. Ruiz Jurado
5429dfbc04 bind publish 2018-08-05 10:44:17 +02:00
Vicente J. Ruiz Jurado
9556bf4e60 New rest methods (subs-union, falsePositive) for mobile 2018-08-05 09:36:18 +02:00
Vicente J. Ruiz Jurado
d4be8c7c46 Privacy updated 2018-08-04 18:42:37 +02:00
Vicente J. Ruiz Jurado
cfe6c2f570 Better mobile notification desc 2018-08-03 09:05:14 +02:00
Vicente J. Ruiz Jurado
20359a79e3 FCM error handling with Meteor.bind 2018-08-03 08:20:20 +02:00
Vicente J. Ruiz Jurado
8780edc832 Correct ObjectId in fcm message 2018-08-03 08:04:43 +02:00
Vicente J. Ruiz Jurado
dc0df7cf32 Master sends only to mobiles 2018-08-02 23:13:15 +02:00
Vicente J. Ruiz Jurado
bf926514c9 Mobile notifications 2018-08-02 12:42:19 +02:00
vjrj
47e9c18607 More work in REST api deletion 2018-06-19 10:49:53 +02:00
vjrj
5642b106d4 Improved rest api calls 2018-06-19 00:18:18 +02:00
vjrj
5b3d86e6a5 Changed rest params in subs 2018-06-18 11:58:04 +02:00
59 changed files with 2705 additions and 1626 deletions

5
.gitignore vendored
View file

@ -12,3 +12,8 @@ public/locales/undefined/
.screenshots
.vscode
output.json
# fase-0 higiene: variantes de settings con secretos (no versionar)
settings-production-*.json
settings-tests.json
*.json~

View file

@ -8,7 +8,12 @@ This is web service that notifies about fires detected in an area of your intere
Prerrequisites: a running meteor (we share the `fuegos` database with the telegram bot).
Install `meteor` and run `npm start`
Install `meteor` and run `meteor npm install` and `npm start`
Some other development deps (in debian & ubuntu):
```
apt-get install libcairo2-dev libjpeg-dev libgif-dev pkg-config
```
### GeoIP
@ -28,14 +33,19 @@ TEST_PORT=3000 TEST_WATCH=1 TEST_CLIENT=0 MONGO_URL=mongodb://localhost:27017/fu
# and
chimp --watch --ddp=http://localhost:3000 --path=cucumber
node_modules/.bin/chimp --watch --ddp=http://localhost:3000 --path=cucumber
# and
chimp --ddp=http://localhost:3000 --path=cucumber
node_modules/.bin/chimp --ddp=http://localhost:3000 --path=cucumber
```
### FAQ & Troubleshooting
**Q** - I get something like `(...) /node_modules/fibers/future.js:280 (...) Error: Could not locate the bindings file.` What can I do?
**A** - Try `meteor npm rebuild`
## Data source acknowledgements
*We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*.
@ -46,4 +56,5 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we
## Thanks & other acknowlegments
(...)
Thanks indeed to:
- Lupe Bao Reixa for Galician translation

29
cucumber/chimp-config.js Normal file
View file

@ -0,0 +1,29 @@
module.exports = {
// - - - - CHIMP - - - -
/* watch: false,
* watchTags: '@watch',
* offline: false,
*/
// - - - - CUCUMBER - - - -
/* path: './features',
* jsonOutput: 'output.json', */
// '- - - - DEBUGGING - - - -
log: 'info',
debug: false,
seleniumDebug: false,
webdriverLogLevel: false,
// debugBrkCucumber: 5858,
// - - - - WEBDRIVER-IO - - - -
webdriverio: {
waitforTimeout: 10000,
waitforInterval: 250, // KEEP SMALL (!!!) this is the INTERVAL in which waitFor* is looped fo
desiredCapabilities: {
chromeOptions: {
// args: ["headless", "disable-gpu"]
args: ['--disable-gpu', '--no-sandbox', '--headless']
},
isHeadless: true
}
}
};

View file

@ -40,7 +40,6 @@ Feature: Test all secundary pages
| fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false |
Then I check that all page urls works properly
@watch
Scenario: Check that other non visible pages work well
Given I logout if logged
And a list of non visible pages ids and contents

View file

@ -1,5 +1,6 @@
/* eslint-disable consistent-return */
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import firesCommonSchema from '../Common/FiresSchema';
@ -18,8 +19,11 @@ ActiveFires.deny({
remove: () => true
});
const activeFiresSchema = firesCommonSchema;
ActiveFires.schema = new SimpleSchema(firesCommonSchema);
activeFiresSchema.fireUnion = { type: Meteor.Collection.ObjectID, optional: true, blackbox: true };
ActiveFires.schema = new SimpleSchema(activeFiresSchema);
ActiveFires.attachSchema(ActiveFires.schema);

View file

@ -32,7 +32,9 @@ const findFiresInRegion = (zone) => {
export const countRealFires = (firesCursor) => {
const realFires = [];
firesCursor.forEach((fire) => {
if (debug) console.log(`${JSON.stringify(fire)} -----`);
const union = firesUnion([fire]);
if (debug) console.log(`${JSON.stringify(union)} -----`);
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
if (falsePos.count() === 0 && industries.count() === 0) {

View file

@ -31,7 +31,8 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat, wit
lat: 1,
lon: 1,
when: 1,
scan: 1
scan: 1,
track: 1
}
});

View file

@ -0,0 +1,71 @@
/* eslint-disable consistent-return */
/* eslint-disable import/no-absolute-path */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
const ActiveFiresUnion = new Mongo.Collection('activefiresunion', { idGeneration: 'MONGO' });
/* Sample:
*
* TODO
*
* */
// We have to store polygons and their centroids
// {"type":"Polygon","coordinates":[[[-5.8645,43.5524],[-5.8621,43.5524],[-5.8621,43.5546],[-5.8645,43.5546],[-5.8645,43.5524]]]}
ActiveFiresUnion.allow({
insert: () => false,
update: () => false,
remove: () => false
});
ActiveFiresUnion.deny({
insert: () => true,
update: () => true,
remove: () => true
});
// https://www.npmjs.com/package/simpl-schema
const GeoJsonSchema = new SimpleSchema({
type: {
type: String,
allowedValues: ['Polygon']
},
coordinates: {
type: Array
},
'coordinates.$': {
type: Array
},
'coordinates.$.$': {
type: Array
},
'coordinates.$.$.$': {
type: Number
}
});
const firesUnionSchema = {
centerid: LocationSchema,
// https://docs.mongodb.com/manual/reference/geojson/
// <field>: { type: <GeoJSON type> , coordinates: <coordinates> }
shape: GeoJsonSchema,
history: { type: Array, optional: true },
'history.$': GeoJsonSchema,
when: Date, // First date
createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt
};
ActiveFiresUnion.schema = new SimpleSchema(firesUnionSchema);
ActiveFiresUnion.attachSchema(ActiveFiresUnion.schema);
export default ActiveFiresUnion;

View file

@ -0,0 +1,80 @@
/* global Counter */
/* eslint-disable import/no-absolute-path */
/* eslint-disable prefer-arrow-callback */
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { NumberBetween } from '/imports/modules/server/other-checks';
// import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications';
// import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
// import Industries from '/imports/api/Industries/Industries';
import ActiveFiresUnion from '../ActiveFiresUnion';
const counter = new Counter('countActiveFiresUnion', ActiveFiresUnion.find({}));
Meteor.publish('activefiresuniontotal', function total() {
return counter;
});
const activeFiresUnion = (b1, b2, c1, c2, withMarks) => {
// a --- b
// c --- d
const geometry = {
$geometry: {
type: 'Polygon',
coordinates: [[
[c1, c2],
[c1, b2],
[b1, b2],
[b1, c2],
[c1, c2]
]]
}
};
// console.log(JSON.stringify(geometry));
const fires = ActiveFiresUnion.find({
shape: {
$geoIntersects: geometry
}
}, {
fields: {
shape: 1,
centerid: 1,
when: 1,
createdAt: 1,
updatedAt: 1
}
});
if (Meteor.isDevelopment) console.log(`Active fires union total: ${fires.count()}`);
/*
if (withMarks && fires.fetch().length > 0) {
const union = firesUnion(fires);
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
return [fires, falsePos, industries];
} */
return fires;
};
Meteor.publish('activefiresunionmyloc', function activeInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat, withMarks) {
// latitude -90 and 90 and the longitude between -180 and 180
check(northEastLng, NumberBetween(-180, 180));
check(southWestLat, NumberBetween(-90, 90));
check(southWestLng, NumberBetween(-180, 180));
check(northEastLat, NumberBetween(-90, 90));
check(withMarks, Boolean);
// I use this for fire stats
// if (!Meteor.isDevelopment) return this.ready(); // empty
return activeFiresUnion(northEastLng, northEastLat, southWestLng, southWestLat, withMarks);
});
// Warning: this increase always by one the fire stats
Meteor.publish('lastFireUnionDetected', function lastFireDetected() {
// I use this for fire stats
// if (!Meteor.isDevelopment) return this.ready(); // empty
return ActiveFiresUnion.find({}, { limit: 1, sort: { when: -1 } });
});

View file

@ -6,6 +6,29 @@ import FalsePositiveTypes from './FalsePositiveTypes';
import Fires from '../Fires/Fires';
import rateLimit from '../../modules/rate-limit';
export function upsertFalsePositive(keyType, owner, fire) {
const fireType = fire.type;
if (fireType === 'vecinal') {
throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego');
}
const date = new Date();
const formated = moment(date).format('YYYYMMDD');
try {
const set = {
owner,
type: keyType,
when: date,
whendateformat: formated,
fireId: fire._id,
geo: fire.ourid
};
return FalsePositives.upsert({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true });
} catch (exception) {
console.log(exception);
throw new Meteor.Error('500', exception);
}
}
Meteor.methods({
'falsePositives.insert': function falsePositivesInsert(fireId, keyType) {
check(fireId, Meteor.Collection.ObjectID);
@ -17,27 +40,9 @@ Meteor.methods({
}
check(this.userId, String);
const fire = Fires.findOne(fireId);
const owner = this.userId;
if (fire) {
const fireType = fire.type;
if (fireType === 'vecinal') {
throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego');
}
const date = new Date();
const formated = moment(date).format('YYYYMMDD');
try {
const set = {
owner: this.userId,
type: keyType,
when: date,
whendateformat: formated,
fireId,
geo: fire.ourid
};
return FalsePositives.upsert({ geo: fire.ourid, owner: this.userId }, { $set: set }, { multi: false, upsert: true });
} catch (exception) {
console.log(exception);
throw new Meteor.Error('500', exception);
}
upsertFalsePositive(keyType, owner, fire);
} else {
throw new Meteor.Error('500', 'Fuego no encontrado');
}

View file

@ -5,7 +5,8 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { NumberBetween } from '/imports/modules/server/other-checks';
import L from 'leaflet-headless';
import '/imports/startup/server/leaflet-workaround.js';
import L from 'leaflet';
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
import Industries from '/imports/api/Industries/Industries';
import FalsePositives from '../FalsePositives';
@ -17,13 +18,26 @@ Meteor.publish('falsePositivesTotal', function total() {
});
export const firesUnion = (fires) => {
const group = new L.FeatureGroup();
const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor
const remap = firesArray.map(function remap(doc) {
// default scan: 1 for neightbor alerts
return { location: { lat: doc.lat, lon: doc.lon }, distance: doc.scan || 1 };
const isNASA = doc.type === 'modis' || doc.type === 'viirs';
const pixelSize = doc.type === 'viirs' ? 0.375 : 1; // viirs has 375m pixel size, modis 1000m
// default 1 km for neightbor alerts
const map = {
location: { lat: doc.lat, lon: doc.lon },
distance: isNASA ? doc.scan * pixelSize : 1,
distanceY: isNASA ? doc.track * pixelSize : 1
};
// console.log(map);
return map;
});
const union = calcUnion(remap, group, sub => sub);
const union = calcUnion(L, remap, sub => sub, false);
return union;
};
export const zoneToUnion = (lat, lon, distance) => {
const remap = [{ location: { lat, lon }, distance }];
const union = calcUnion(L, remap, sub => sub, true);
return union;
};

View file

@ -33,7 +33,8 @@ Meteor.publish('fireAlerts', function fireAlerts(northEastLng, northEastLat, sou
fields: {
lat: 1,
lon: 1,
scan: 1
scan: 1,
track: 1
}
});
if (Meteor.isDevelopment) console.log(`Neighbour alerts total: ${fires.count()}`);

View file

@ -139,35 +139,40 @@ function logUrl(fireEnc, params) {
ravenLogger.log(message);
}
Meteor.publish('fireFromHash', function fireFromHash(fireEnc, params) {
export function fireFromHash(fireEnc, params) {
check(fireEnc, String);
check(params, Object);
// console.log(fireEnc);
// const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
const unsealed = Promise.await(unsealW(fireEnc));
if (unsealed === undefined) {
throw Error(`Fail to unseal '${fireEnc}'`);
}
const w = unsealed.when;
unsealed.when = new Date(w);
const c = unsealed.createdAt;
unsealed.createdAt = !c ? new Date() : new Date(c);
const u = unsealed.updatedAt;
unsealed.updatedAt = !u ? new Date() : new Date(u);
// console.log(unsealed);
// FIXME:
const unsealedFix = fixConfidence(unsealed);
FiresCollection.schema.validate(unsealedFix);
return findOrCreateFire(unsealedFix);
/* console.log(`fires: ${fire.count()}`);
* return fire; */
}
Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) {
check(fireEnc, String);
check(params, Object);
try {
// console.log(fireEnc);
// const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
const unsealed = Promise.await(unsealW(fireEnc));
if (unsealed === undefined) {
logUrl(fireEnc, params);
// https://guide.meteor.com/data-loading.html
return this.ready();
}
const w = unsealed.when;
unsealed.when = new Date(w);
const c = unsealed.createdAt;
unsealed.createdAt = !c ? new Date() : new Date(c);
const u = unsealed.updatedAt;
unsealed.updatedAt = !u ? new Date() : new Date(u);
// console.log(unsealed);
// FIXME:
const unsealedFix = fixConfidence(unsealed);
FiresCollection.schema.validate(unsealedFix);
return findOrCreateFire(unsealedFix);
/* console.log(`fires: ${fire.count()}`);
* return fire; */
return fireFromHash(fireEnc, params);
} catch (e) {
console.warn(e);
logUrl(fireEnc, params);
// https://guide.meteor.com/data-loading.html
return this.ready();
}
});

View file

@ -2,6 +2,7 @@
/* eslint-disable import/no-absolute-path */
import { Mongo } from 'meteor/mongo';
import { Meteor } from 'meteor/meteor';
import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
@ -22,11 +23,12 @@ Notifications.deny({
Notifications.schema = new SimpleSchema({
userId: String,
subsId: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true },
content: String,
geo: LocationSchema,
type: String,
webNotified: { type: Boolean, optional: true },
webNotifiedAt: { type: Date, optional: true },
notified: { type: Boolean, optional: true },
notifiedAt: { type: Date, optional: true },
emailNotified: { type: Boolean, optional: true },
emailNotifiedAt: { type: Date, optional: true },
when: Date,

View file

@ -8,7 +8,7 @@ Meteor.methods({
check(notifId, Meteor.Collection.ObjectID);
try {
Notifications.update(notifId, { $set: { webNotified: true, webNotifiedAt: new Date() } });
Notifications.update(notifId, { $set: { notified: true, notifiedAt: new Date() } });
return notifId;
} catch (exception) {
throw new Meteor.Error('500', exception);

View file

@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
import Notifications from '../Notifications';
Meteor.publish('mynotifications', function notifications() {
const notif = Notifications.find({ userId: this.userId, type: 'web', webNotified: null });
const notif = Notifications.find({ userId: this.userId, type: 'web', notified: null });
// console.log(`Notifications for user ${this.userId}: ${notif.count()}`);
return notif;
});

View file

@ -8,13 +8,16 @@ import { check } from 'meteor/check';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import { countRealFires } from '/imports/api/ActiveFires/server/countFires';
import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications';
import { whichAreFalsePositives, firesUnion, zoneToUnion } from '/imports/api/FalsePositives/server/publications';
import { fireFromHash } from '/imports/api/Fires/server/publications.js';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries';
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
import jsend from 'jsend';
import { Random } from 'meteor/random';
import { subscriptionsInsert } from '/imports/api/Subscriptions/methods.js';
import { Mongo } from 'meteor/mongo';
import { upsertFalsePositive } from '/imports/api/FalsePositives/methods.js';
const debug = false;
@ -22,6 +25,11 @@ const uptime = new Date();
const restivusError = (code, message) => ({ status: 'error', statusCode: code, body: message });
function failMsg(msg) {
return restivusError(500, `Unexpected error in REST call: ${msg}`);
}
function fail(e) {
return restivusError(500, `Unexpected error in REST call: ${e}`);
}
@ -149,32 +157,35 @@ if (!Meteor.settings.private.internalApiToken) {
lat: 1,
lon: 1,
when: 1,
scan: 1
type: 1, // modis, viirs, vecinal
scan: 1,
track: 1
}
});
const result = { total: fires.count(), real: countRealFires(fires) };
if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius ${result}`);
if (!full) {
return result;
}
let union;
// TODO only get real
const firesA = fires.fetch();
if (firesA.length > 0) {
const union = firesUnion(fires);
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
result.fires = firesA;
result.industries = industries.fetch();
result.falsePos = falsePos.fetch();
return result;
}
// otherwise
return {
total: 0, real: 0, fires: [], falsePos: [], industries: []
};
if (firesA.length > 0) {
union = firesUnion(fires);
} else {
union = zoneToUnion(lat, lng, km);
}
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
result.fires = firesA;
result.industries = industries.fetch();
result.falsePos = falsePos.fetch();
return result;
}
// Maps to: /api/v1/fires-in/:lat/:lng/:km
@ -270,18 +281,24 @@ if (!Meteor.settings.private.internalApiToken) {
const {
token,
mobileToken,
id,
lat,
lon,
distance
} = this.bodyParams;
let oid;
try {
check(token, String);
check(mobileToken, String);
check(id, String);
oid = new Mongo.ObjectID(id);
check(oid, Meteor.Collection.ObjectID);
check(lat, Number);
check(lon, Number);
check(distance, Number);
checkLatLonDist(distance, lat, lon);
} catch (e) {
if (debug) console.log(e);
return defaultFailParams(e);
}
@ -289,9 +306,10 @@ if (!Meteor.settings.private.internalApiToken) {
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
if (!user) return failMsg('User not found');
const newSubs = {};
newSubs._id = new Meteor.Collection.ObjectID(id);
newSubs.location = {};
newSubs.location.lat = lat;
newSubs.location.lon = lon;
@ -304,13 +322,16 @@ if (!Meteor.settings.private.internalApiToken) {
return fail(e);
}
return jsend.success({ subsId: result._str });
},
}
});
apiV1.addRoute('mobile/subscriptions/:token/:mobileToken/:subsId', { authRequired: false }, {
delete: function del() {
const {
token,
mobileToken,
subsId
} = this.bodyParams;
} = this.urlParams;
try {
check(token, String);
check(mobileToken, String);
@ -323,7 +344,7 @@ if (!Meteor.settings.private.internalApiToken) {
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
if (!user) return failMsg('User not found');
try {
Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) });
@ -335,9 +356,9 @@ if (!Meteor.settings.private.internalApiToken) {
}
});
apiV1.addRoute('mobile/subscriptions/all', { authRequired: false }, {
apiV1.addRoute('mobile/subscriptions/all/:token/:mobileToken', { authRequired: false }, {
get: function get() {
const { token, mobileToken } = this.bodyParams;
const { token, mobileToken } = this.urlParams;
try {
check(token, String);
check(mobileToken, String);
@ -349,14 +370,14 @@ if (!Meteor.settings.private.internalApiToken) {
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
if (!user) return failMsg('User not found');
const result = Subscriptions.find({ owner: user._id });
return jsend.success({ subscriptions: result.fetch(), count: result.count() });
},
delete: function delAll() {
const { token, mobileToken } = this.bodyParams;
const { token, mobileToken } = this.urlParams;
try {
check(token, String);
check(mobileToken, String);
@ -365,16 +386,72 @@ if (!Meteor.settings.private.internalApiToken) {
}
const failed = checkAuthToken(token);
if (failed) return failed;
if (failed) return failed('Auth api check failed');
if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return failed;
if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return fail;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failed;
if (!user) return failMsg('User not found');
const toRemove = Subscriptions.find({ owner: user._id }).count();
Subscriptions.remove({ owner: user._id });
return jsend.success({ count: toRemove });
}
});
apiV1.addRoute('status/subs-public-union/:token', { authRequired: false }, {
get: function get() {
const { token } = this.urlParams;
try {
check(token, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const currentUnion = SiteSettings.findOne({ name: 'subs-public-union' });
const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' });
return jsend.success({ union: currentUnion, bounds: userSubsBounds });
}
});
apiV1.addRoute('mobile/falsepositive', { authRequired: false }, {
post: function post() {
const {
token,
mobileToken,
sealed,
type
} = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
check(sealed, String);
check(type, String);
} catch (e) {
if (debug) console.log(e);
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failMsg('User not found');
console.log(`Marking hash fire as '${type}' false positive: ${sealed}`);
const fireC = fireFromHash(sealed, { id: sealed });
if (fireC && typeof fireC === 'object') {
const fire = fireC.fetch()[0];
console.log(`Marking fire as false positive: ${fire._id}`);
const result = upsertFalsePositive(type, user._id, fire);
return jsend.success({ upsert: result });
}
return failMsg('Cannot mark fire as false positive');
}
});
}

View file

@ -1,6 +1,7 @@
/* eslint-disable consistent-return */
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
@ -42,6 +43,7 @@ Subscriptions.deny({
Subscriptions.schema = new SimpleSchema({
_id: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true },
location: Object,
'location.lat': Number,
'location.lon': Number,

View file

@ -12,6 +12,7 @@ function geo(doc) {
export function subscriptionsInsert(doc, userId, type) {
check(doc, {
_id: Match.Maybe(Meteor.Collection.ObjectID),
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
distance: Number
});

View file

@ -0,0 +1,153 @@
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import Notifications from '/imports/api/Notifications/Notifications';
import i18n from 'i18next';
import moment from 'moment';
import { dateLongFormat } from '/imports/api/Common/dates';
// import sendMail from '/imports/startup/server/email';
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
import { isMailServerMaster } from '/imports/startup/server/email';
// import { hr } from '/imports/startup/server/email';
import getEmailOf from '/imports/modules/get-email-of-user';
import image from 'google-maps-image-api-url';
import gcm from 'node-gcm';
import { trim } from '/imports/ui/components/NotificationsObserver/util.js';
import ravenLogger from '/imports/startup/server/ravenLogger';
let validFcmSender = true;
if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) {
console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work');
validFcmSender = false;
}
// https://www.npmjs.com/package/google-maps-image-api-url
// https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email
// https://developers.google.com/maps/documentation/static-maps/intro
function imgUrl(lat, lng) {
return image({
key: Meteor.settings.gmaps.key,
type: 'staticmap',
center: `${lat},${lng}`,
size: '640x480',
zoom: 16,
maptype: 'hybrid',
language: 'es',
markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}`
});
}
/*
function imgEl(lat, lng) {
return `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
} */
// Cutover flag (fase 1a): channels listed in
// Meteor.settings.private.notifDisabledChannels (e.g. ['mobile','web']) are
// handled by the tcef-notifications microservice, so the old observer/cron must
// NOT send them. Mutual exclusion per channel — reversible by editing settings,
// no code deploy needed. ES5-compatible on purpose (Meteor 1.6 / Node 8).
const notifDisabledChannels = (Meteor.settings.private && Meteor.settings.private.notifDisabledChannels) || [];
const processNotif = (notif) => {
if (notifDisabledChannels.indexOf(notif.type) !== -1) {
// This channel was migrated to tcef-notifications; do nothing here.
return;
}
if (isMailServerMaster && validFcmSender && notif.type === 'mobile' && notif.notified !== true) {
const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken);
const user = Meteor.users.findOne({ _id: notif.userId });
moment.locale(user.lang);
// duplicate code below
const body = `${trim(notif.content)}`;
// https://firebase.google.com/docs/cloud-messaging/concept-options
const msg = new gcm.Message();
msg.addNotification({
title: i18n.t('Alerta de fuego'),
body,
click_action: 'FLUTTER_NOTIFICATION_CLICK',
tag: notif._id, // prevent duplication of fire notifications
sound: 'default', // Indicates sound to be played. Supports only default currently.
icon: 'launch_image' // 'ic_launcher'
});
msg.addData('id', notif._id._str);
msg.addData('description', body);
msg.addData('lat', notif.geo.coordinates[1]);
msg.addData('lon', notif.geo.coordinates[0]);
msg.addData('when', notif.when);
msg.addData('subsId', notif.subsId._str);
msg.addData('sealed', notif.sealed);
const registrationTokens = [];
if (!user.fireBaseToken) {
console.warn('This mobile user doesn\'t have a firebase registration token');
} else {
registrationTokens.push(user.fireBaseToken);
// FIXME: better join users
if (validFcmSender) {
fcmSender.send(msg, { registrationTokens }, Meteor.bindEnvironment(function processResult(err, response) {
if (err) {
console.error(`FCM error: ${err}`);
// FIXME send to sentry
ravenLogger.log(err);
} else {
// console.log(`FCM response: ${response}`);
Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } });
}
}));
}
}
}
if (isMailServerMaster && notif.type === 'web' && notif.emailNotified !== true) {
const user = Meteor.users.findOne({ _id: notif.userId });
const { firstName, emailAddress } = getEmailOf(user);
if (emailAddress) {
const img = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
// const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`;
// const fireHtmlUrl = `<a href="${fireUrl}">${i18n.t('Más información sobre este fuego')}</a>`;
// TODO get _id of fire
// const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`;
// FIXME use our map as url and static map as img
moment.locale(user.lang);
// moment user tz ?
const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`;
// TODO Comunes Address
const emailOpts = {
to: emailAddress,
// userName: firstName,
// sendAt: new Date(),
subject: subjectTruncate.apply(message),
// text: `${message}\n\n${fireTextUrl}\n\n`,
// template: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
lang: user.lang,
template: 'new-fire',
templateVars: {
applicationName: i18n.t('AppName'),
firstName,
message,
fireUrl,
img,
subsUrl: Meteor.absoluteUrl('subscriptions')
}
};
sendEmail(emailOpts).catch((error) => {
throw new Meteor.Error('500', `${error}`);
});
// sendMail(emailOpts, true);
Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } });
} else {
// Not email or not verified -> remove notif so we don't retry to send it
Notifications.remove({ _id: notif._id });
}
}
};
export default processNotif;

View file

@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n';
const detectorOptions = {
// order and from where user language should be detected
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
order: ['querystring', 'navigator', 'cookie', 'localStorage', 'htmlTag'],
// keys or params to lookup language from
lookupQuerystring: 'lng',
@ -61,6 +61,14 @@ if (sendMissing && Meteor.isDevelopment) {
};
}
function setT9(lang) {
if (lang === 'gl') {
T9n.setLanguage('es');
} else {
T9n.setLanguage(lang);
}
}
i18n.use(backend)
.use(LngDetector)
.use(Cache)
@ -75,7 +83,7 @@ i18n.use(backend)
// document.title = t('AppName');
// Accounts translation
// https://github.com/softwarerero/meteor-accounts-t9n
T9n.setLanguage(i18n.language);
setT9(i18n.language);
// console.log(T9n.get('error.accounts.User not found'));
moment.locale(i18n.language);
@ -104,7 +112,8 @@ Meteor.subscribe('userData'); // lang is there
i18n.on('languageChanged', (lng) => {
moment.locale(lng);
T9n.setLanguage(lng);
// TODO fix this when gl is translated
setT9(lng);
Tracker.autorun((computation) => {
if (Meteor.userId()) {
// logged

View file

@ -3,6 +3,8 @@ import moment from 'moment';
// Load the js langs
import es from 'meteor-accounts-t9n/build/es';
import en from 'meteor-accounts-t9n/build/en';
// TODO ask for translation of this
// import gl from 'meteor-accounts-t9n/build/gl';
const backOpts = {
// path where resources get loaded from
@ -21,7 +23,10 @@ const shouldDebug = (forceDebug && !Meteor.isProduction);
const i18nOpts = {
backend: backOpts,
// lng: 'es',
fallbackLng: ['es', 'en'],
fallbackLng: {
gl: ['es'],
default: ['en']
},
sendMissingTo: 'fallback',
interpolation: {
escapeValue: false, // not needed for react!!
@ -35,7 +40,7 @@ const i18nOpts = {
return value;
}
},
whitelist: ['es', 'en'], // allowed languages
whitelist: ['es', 'en', 'gl'], // allowed languages
load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en
debug: shouldDebug,
ns: 'common',

View file

@ -9,6 +9,7 @@ import '../../api/Users/server/publications';
import '../../api/Utility/server/methods';
import '../../api/ActiveFires/server/publications';
import '../../api/ActiveFiresUnion/server/publications';
import '../../api/FireAlerts/server/publications';
import '../../api/Subscriptions/methods';

View file

@ -4,6 +4,8 @@
import { Meteor } from 'meteor/meteor';
import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone';
import { isMailServerMaster, sendEmailsFromQueue } from '/imports/startup/server/email';
import Notifications from '/imports/api/Notifications/Notifications';
import processNotif from '/imports/modules/server/notificationsProcess.js';
// https://github.com/thesaucecode/meteor-synced-cron/
@ -44,7 +46,7 @@ Meteor.startup(() => {
timezone: 'Europe/Madrid',
schedule: (parser) => {
// http://bunkat.github.io/later/
const sched = parser.text('every 1 min');
const sched = parser.text('every 10 min');
if (sched.error !== -1) {
console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`);
}
@ -52,6 +54,30 @@ Meteor.startup(() => {
},
job: () => sendEmailsFromQueue()
});
SyncedCron.add({
name: 'Process pending notif',
timezone: 'Europe/Madrid',
schedule: (parser) => {
// http://bunkat.github.io/later/
const sched = parser.text('every 15 min');
if (sched.error !== -1) {
console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`);
}
return sched;
},
job: () => {
const mobileNotif = Notifications.find({ nofitied: null, type: 'mobile' });
mobileNotif.forEach((notif) => {
processNotif(notif);
});
const emailNotif = Notifications.find({ emailNofitied: null, type: 'web' });
emailNotif.forEach((notif) => {
processNotif(notif);
});
return { mobile: mobileNotif.count(), web: emailNotif.count() };
}
});
}
const esEn = Meteor.settings.private.twitter.es.enabled;

View file

@ -17,5 +17,4 @@ import './prerender';
import './feedback';
import './cron';
import './isMaster';
import './segfaults';
import './rest';

View file

@ -0,0 +1,19 @@
// imports/startup/server/leaflet-workaround.js
global.window = {
screen: {
deviceXDPI: 1,
logicalXDPI: 1
}
};
global.document = {
documentElement: {
style: {}
},
createElement: () => {
return {};
}
};
global.navigator = {
userAgent: 'foo',
platform: 'bar'
};

View file

@ -9,6 +9,8 @@ import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries';
import IndustryRegistries from '/imports/api/Industries/IndustryRegistries';
import Notifications from '/imports/api/Notifications/Notifications';
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
import { Mongo } from 'meteor/mongo';
Meteor.startup(() => {
@ -227,6 +229,30 @@ Meteor.startup(() => {
}
});
Migrations.add({
version: 17,
up: function renameWebNotifiedField() {
Notifications.update({ webNotified: { $exists: true } }, {
$rename: { webNotifiedAt: 'notifiedAt' }
}, { upsert: false, multi: true });
Notifications.update({ webNotified: { $exists: true } }, {
$rename: { webNotified: 'notified' }
}, { upsert: false, multi: true });
}
});
Migrations.add({
version: 18,
up: () => {
const raw = ActiveFiresUnion.rawCollection();
raw.createIndex({ centerid: '2dsphere' });
raw.createIndex({ shape: '2dsphere' });
raw.createIndex({ when: 1 });
raw.createIndex({ createdAt: 1 });
raw.createIndex({ updatedAt: 1 });
}
});
// Set createdAt in users & subs
Migrations.migrateTo('latest');

View file

@ -1,92 +1,19 @@
/* eslint-disable import/no-absolute-path */
import i18n from 'i18next';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { dateLongFormat } from '/imports/api/Common/dates';
import Notifications from '/imports/api/Notifications/Notifications';
// import sendMail from '/imports/startup/server/email';
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
// import { hr } from '/imports/startup/server/email';
import getEmailOf from '/imports/modules/get-email-of-user';
import image from 'google-maps-image-api-url';
import { trim } from '/imports/ui/components/NotificationsObserver/util.js';
import processNotif from '/imports/modules/server/notificationsProcess.js';
import { isMailServerMaster } from '/imports/startup/server/email';
Meteor.startup(() => {
// https://www.npmjs.com/package/google-maps-image-api-url
// https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email
// https://developers.google.com/maps/documentation/static-maps/intro
function imgUrl(lat, lng) {
return image({
key: Meteor.settings.gmaps.key,
type: 'staticmap',
center: `${lat},${lng}`,
size: '640x480',
zoom: 16,
maptype: 'hybrid',
language: 'es',
markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}`
if (isMailServerMaster) {
Notifications.find().observe({
added: function notifAdded(notif) {
processNotif(notif);
},
changed: function notifChanged(updatedNotif) { // , oldNotif) {
processNotif(updatedNotif);
}
});
}
/*
function imgEl(lat, lng) {
return `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
} */
function process(notif) {
if (notif.type === 'web' && !notif.emailNotified) {
const user = Meteor.users.findOne({ _id: notif.userId });
const { firstName, emailAddress } = getEmailOf(user);
if (emailAddress) {
const img = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
// const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`;
// const fireHtmlUrl = `<a href="${fireUrl}">${i18n.t('Más información sobre este fuego')}</a>`;
// TODO get _id of fire
// const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`;
// FIXME use our map as url and static map as img
moment.locale(user.lang);
// moment user tz ?
const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`;
// TODO Comunes Address
const emailOpts = {
to: emailAddress,
// userName: firstName,
// sendAt: new Date(),
subject: subjectTruncate.apply(message),
// text: `${message}\n\n${fireTextUrl}\n\n`,
// template: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
lang: user.lang,
template: 'new-fire',
templateVars: {
applicationName: i18n.t('AppName'),
firstName,
message,
fireUrl,
img,
subsUrl: Meteor.absoluteUrl('subscriptions')
}
};
sendEmail(emailOpts).catch((error) => {
throw new Meteor.Error('500', `${error}`);
});
// sendMail(emailOpts, true);
Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } });
}
}
}
Notifications.find().observe({
added: function notifAdded(notif) {
process(notif);
},
changed: function notifChanged(updatedNotif, oldNotif) {
process(updatedNotif);
}
});
});

View file

@ -1,25 +0,0 @@
import SegfaultHandler from 'segfault-handler';
import moment from 'moment';
// import ravenLogger from './ravenLogger';
// https://github.com/ddopson/node-segfault-handler
const dateFileFormat = moment().format('YYYYMMDD_HH:mm:ss');
SegfaultHandler.registerHandler(`/var/tmp/tcef-stacktrace-${dateFileFormat}.log`);
// This callaback does not work:
// https://github.com/ddopson/node-segfault-handler/issues/49
/* SegfaultHandler.registerHandler('/var/tmp/tcef-crash.log', (signal, address, stack) => {
* // Do what you want with the signal, address, or stack (array)
* // This callback will execute before the signal is forwarded on.
* ravenLogger.log(stack, signal);
* }); */
// Only uncomment for segv tests
// SegfaultHandler.causeSegfault();
// Or this for normal error (should go to raven in configured)
// throw new Error('This is just a test of error in server');

View file

@ -4,6 +4,8 @@
import Fires from '/imports/api/Fires/Fires';
// import Comments from '/imports/api/Comments/Comments';
const firesMapEnabled = false;
sitemaps.add('/sitemap.xml', () => {
const today = new Date();
@ -20,21 +22,23 @@ sitemaps.add('/sitemap.xml', () => {
// When user has public page
/* const users = Meteor.users.find().fetch();
_.each(users, function(user) {
out.push({
page: '/persona/' + user.username,
lastmod: user.updatedAt
});
_.each(users, function(user) {
out.push({
page: '/persona/' + user.username,
lastmod: user.updatedAt
});
}); */
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
// Search the last comment of tha fire
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
out.push({
page: `/fire/archive/${fire._id._str}`,
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
if (firesMapEnabled) {
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
// Search the last comment of tha fire
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
out.push({
page: `/fire/archive/${fire._id._str}`,
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
});
});
});
}
return out;
});

View file

@ -4,7 +4,8 @@ import { Meteor } from 'meteor/meteor';
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import Perlin from 'loms.perlin';
import L from 'leaflet-headless';
import './leaflet-workaround';
import L from 'leaflet';
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
import { isMailServerMaster } from '/imports/startup/server/email';
@ -35,9 +36,8 @@ Meteor.startup(() => {
const noNoisy = sub => sub;
const process = (isPublic) => {
const group = new L.FeatureGroup();
const subscribers = Subscriptions.find().fetch();
const result = calcUnion(subscribers, group, isPublic ? addNoisy : noNoisy);
const result = calcUnion(L, subscribers, isPublic ? addNoisy : noNoisy, true);
const union = result[0];
const bounds = result[1];

View file

@ -3,7 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { Line } from 'react-progressbar.js';
import { Line } from 'react-progress-bar.js';
import './LoadingBar.scss';

View file

@ -35,7 +35,8 @@ class DefMapLayers extends Component {
<TileLayer
opacity={defOpacity}
attribution="&copy; <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors"
url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
// url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
/>
</BaseLayer>);
const osmlayer = (

View file

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import { Circle } from 'react-leaflet';
import { GeoJSON } from 'react-leaflet';
import PropTypes from 'prop-types';
import { rectangleAround } from 'map-common-utils';
import { onMarkClick } from './MarkListeners';
import FirePopup from './FirePopup';
class FireCircleMark extends Component {
constructor(props) {
super(props);
@ -22,22 +22,26 @@ class FireCircleMark extends Component {
lat,
lon,
scan,
track,
nasa,
id,
history,
when,
t
} = this.props;
const rect = rectangleAround({ lat, lon }, track, track);
return (
<Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 500} onClick={this.onClick}>
<GeoJSON data={rect} color="red" stroke width="1" opacity=".4" fillOpacity="1">
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</Circle>
</GeoJSON>
);
/* <Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 500} onClick={this.onClick}> */
}
}
FireCircleMark.propTypes = {
scan: PropTypes.number.isRequired,
track: PropTypes.number.isRequired,
lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired,
nasa: PropTypes.bool.isRequired,

View file

@ -3,8 +3,8 @@ import React, { Component, Fragment } from 'react';
import { CircleMarker, Marker, Tooltip } from 'react-leaflet';
import PropTypes from 'prop-types';
import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons';
import { onMarkClick } from './MarkListeners';
import { translate } from 'react-i18next';
import { onMarkClick } from './MarkListeners';
import FirePopup from './FirePopup';
class FireIconMark extends Component {
@ -30,17 +30,7 @@ class FireIconMark extends Component {
render() {
const {
lat,
lon,
scan,
nasa,
id,
history,
falsePositives,
industries,
neighbour,
when,
t
lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t
} = this.props;
return (
<div>
@ -79,6 +69,7 @@ FireIconMark.propTypes = {
lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired,
scan: PropTypes.number,
track: PropTypes.number,
nasa: PropTypes.bool,
falsePositives: PropTypes.bool.isRequired,
industries: PropTypes.bool.isRequired,

View file

@ -0,0 +1,22 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
import React from 'react';
import PropTypes from 'prop-types';
import FirePolygonMark from '/imports/ui/components/Maps/FirePolygonMark';
export default function FireListUnion(props) {
const {
firesUnion, nasa, t, history
} = props;
/* console.log(`Using marks: ${useMarks}, using pixels: ${usePixel}`); */
const items = firesUnion.map(({ _id, ...otherProps }) => (<FirePolygonMark t={t} history={history} id={_id} key={_id} nasa={nasa} {...otherProps} />));
return (<div style={{ display: 'none' }}>{items}</div>);
}
FireListUnion.propTypes = {
firesUnion: PropTypes.array.isRequired,
nasa: PropTypes.bool.isRequired,
history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};

View file

@ -3,31 +3,32 @@
import React from 'react';
import { CircleMarker } from 'react-leaflet';
import PropTypes from 'prop-types';
import FirePopup from './FirePopup';
import { translate } from 'react-i18next';
import FirePopup from './FirePopup';
/* Less acurate (1 pixel per fire) but faster */
const FirePixel = ({
lat, lon, nasa, id, when, t, history
lat, lon, nasa, id, when, t, history, track
}) => (
<CircleMarker
center={[lat, lon]}
color={nasa ? 'red' : '#D35400'}
stroke={false}
fillOpacity="1"
fill
radius={nasa ? 1 : 2}
center={[lat, lon]}
color={nasa ? 'red' : '#D35400'}
stroke={false}
fillOpacity="1"
fill
radius={nasa ? 1 : 2}
>
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</CircleMarker>
);
FirePixel.propTypes = {
lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired,
id: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
scan: PropTypes.number,
track: PropTypes.number,
when: PropTypes.instanceOf(Date),
nasa: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired

View file

@ -0,0 +1,48 @@
import React, { Component } from 'react';
import { GeoJSON } from 'react-leaflet';
import PropTypes from 'prop-types';
import { onMarkClick } from './MarkListeners';
import FirePopup from './FirePopup';
class FirePolygonMark extends Component {
constructor(props) {
super(props);
this.t = props.t;
this.onClick = this.onClick.bind(this);
}
onClick() {
const { history, nasa, id } = this.props;
onMarkClick(history, nasa, id);
}
render() {
const {
nasa,
id,
shape,
centerid,
history,
when,
t
} = this.props;
const lon = centerid.coordinates[0];
const lat = centerid.coordinates[1];
return (
<GeoJSON data={shape} color="orange" stroke width="1" opacity=".2" fillOpacity=".0" />
);
/* <FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} /> */
}
}
FirePolygonMark.propTypes = {
id: PropTypes.object.isRequired,
nasa: PropTypes.bool.isRequired,
shape: PropTypes.object.isRequired,
centerid: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
when: PropTypes.instanceOf(Date).isRequired,
t: PropTypes.func.isRequired
};
export default FirePolygonMark;

View file

@ -33,8 +33,7 @@ const subsUnion = (union, options) => {
options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest));
}
} else if (options.subs.length > 0) {
const unionGroup = new L.FeatureGroup();
const result = calcUnion(options.subs, unionGroup, sub => sub);
const result = calcUnion(L, options.subs, sub => sub, true);
const unionJson = result[0];
const bounds = result[1];

View file

@ -1,48 +1,9 @@
import { check } from 'meteor/check';
import LGeo from 'leaflet-geodesy';
import tunion from '@turf/union';
import ttrunc from '@turf/truncate';
import { calcUnion as calcUnionWoBounds } from 'map-common-utils';
// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles
const truncOptions = { precision: 6, coordinates: 2 };
const unify = (polyList) => {
let unionTemp;
for (let i = 0; i < polyList.length; i += 1) {
const pol = polyList[i].toGeoJSON();
const cleanPol = ttrunc(pol, truncOptions);
if (i === 0) {
unionTemp = cleanPol;
} else {
unionTemp = ttrunc(tunion(unionTemp, cleanPol), truncOptions);
}
}
return unionTemp;
};
const calcUnion = (subs, group, decorated) => {
const unionGroup = group;
const copts = {
parts: 144
};
subs.forEach((osub) => {
try {
if (osub.location && osub.location.lat && osub.location.lon && osub.distance) {
check(osub.location.lon, Number);
check(osub.location.lat, Number);
check(osub.distance, Number);
const dsub = decorated(osub);
const circle = LGeo.circle([dsub.location.lat, dsub.location.lon], dsub.distance * 1000, copts);
circle.addTo(unionGroup);
} else {
console.info(`Wrong subscription ${JSON.stringify(osub)}`);
}
} catch (e) {
console.error(e, `Wrong subscription trying to make union ${JSON.stringify(osub)}`);
}
});
const unionJson = unify(unionGroup.getLayers());
return [unionJson, unionGroup.getBounds()];
const calcUnion = (L, subs, decorated, typeCircle) => {
const unionJson = calcUnionWoBounds(subs, decorated, typeCircle);
// return [unionJson, L.geoJSON(unionJson).getBounds()];
return [unionJson, unionJson === null ? null : L.geoJSON(unionJson).getBounds()];
};
export default calcUnion;

View file

@ -12,7 +12,7 @@ import { trim } from './util.js';
function process(notif) {
// No already notified
if (Push.Permission.has()) {
if (!notif.webNotified) {
if (!notif.notified && notif.type === 'web') {
Push.create(i18n.t('AppName'), {
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`,
icon: '/n-fire-marker.png',
@ -47,7 +47,7 @@ Meteor.startup(() => {
if (Meteor.userId()) {
Meteor.subscribe('mynotifications');
// Check for notifications not processed at startup
Notifications.find({ webNotified: null }).forEach((notif) => {
Notifications.find({ notified: null, type: 'web' }).forEach((notif) => {
process(notif);
});
}

View file

@ -98,66 +98,66 @@ const App = props => (
/* https://react.i18next.com/components/i18nextprovider.html */
<div>
{props.i18nReady.get() &&
<ErrorBoundary>
<I18nextProvider i18n={i18n}>
<ErrorBoundary appName={i18n.t('AppNameFull')} title={i18n.t('general-error-title')} subTitle={i18n.t('general-error-description')}>
<Router history={history}>
<LocationListener>
{ !props.loading &&
<div className="App">
<Helmet>
<meta charSet="utf-8" />
<title>{i18n.t('AppName')}</title>
<meta name="description" content={`${i18n.t('AppDescrip')}: ${i18n.t('AppDescripLong')}`} />
<link rel="alternate" href={`https://fires.comunes.org${history.location.pathname}`} hrefLang="en" />
<link rel="alternate" href={`https://fuegos.comunes.org${history.location.pathname}`} hrefLang="es" />
</Helmet>
<Navigation {...props} />
<ReSendEmail {...props} />
<ErrorBoundary>
<I18nextProvider i18n={i18n}>
<ErrorBoundary appName={i18n.t('AppNameFull')} title={i18n.t('general-error-title')} subTitle={i18n.t('general-error-description')}>
<Router history={history}>
<LocationListener>
{ !props.loading &&
<div className="App">
<Helmet>
<meta charSet="utf-8" />
<title>{i18n.t('AppName')}</title>
<meta name="description" content={`${i18n.t('AppDescrip')}: ${i18n.t('AppDescripLong')}`} />
<link rel="alternate" href={`https://fires.comunes.org${history.location.pathname}`} hrefLang="en" />
<link rel="alternate" href={`https://fuegos.comunes.org${history.location.pathname}`} hrefLang="es" />
</Helmet>
<Navigation {...props} />
<ReSendEmail {...props} />
<Grid>
<Switch>
<Route exact name="index" path="/" component={Index} />
<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} />
<Authenticated exact path="/status" component={Status} {...props} />
<Route path="/fires" component={FiresMap} industries={false} {...props} />
<Route path="/zones" component={ZonesMap} {...props} />
<Grid>
<Switch>
<Route exact name="index" path="/" component={Index} />
<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} />
<Authenticated exact path="/status" component={Status} {...props} />
<Route path="/fires" component={FiresMap} industries={false} {...props} />
<Route path="/zones" component={ZonesMap} {...props} />
<Route path="/fire/:type(active|archive|alert)/:id" component={Fires} {...props} />
<Route path="/fire/:id" component={Fires} {...props} />
<Public path="/auth/:token" component={Auth} {...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="/error" component={TestError} {...props} />
<Route path="/fire/:type(active|archive|alert)/:id" component={Fires} {...props} />
<Route path="/fire/:id" component={Fires} {...props} />
<Public path="/auth/:token" component={Auth} {...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="/error" component={TestError} {...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} />
<Route name="terms" path="/terms" component={Terms} />
<Route name="privacy" path="/privacy" component={Privacy} />
<Route name="license" path="/license" component={License} />
<Route name="credits" path="/credits" component={Credits} />
<Route name="about" path="/about" component={About} />
<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} />
<Route name="terms" path="/terms" component={Terms} />
<Route name="privacy" path="/privacy" component={Privacy} />
<Route name="license" path="/license" component={License} />
<Route name="credits" path="/credits" component={Credits} />
<Route name="about" path="/about" component={About} />
<Route component={NotFound} />
</Switch>
</Grid>
<Footer />
<Reconnect {...props} />
<Feedback {...props} />
{props.i18nReady.get() && <Blaze template="cookieConsent" /> }
</div>}
</LocationListener>
</Router>
</ErrorBoundary>
</I18nextProvider>
</ErrorBoundary> }
<Route component={NotFound} />
</Switch>
</Grid>
<Footer />
<Reconnect {...props} />
<Feedback {...props} />
{props.i18nReady.get() && <Blaze template="cookieConsent" /> }
</div>}
</LocationListener>
</Router>
</ErrorBoundary>
</I18nextProvider>
</ErrorBoundary> }
</div>
);
@ -186,7 +186,9 @@ export default withTracker(() => {
const loading = !Roles.subscription.ready() || !i18nReady.get();
const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
const emailAddress = user && user.emails && user.emails[0].address;
// console.log(`i18n ready?: ${i18nReady.get()}`);
Meteor.autorun(() => {
console.log(`i18n ready?: ${i18nReady.get()}`);
});
return {
loading,
loggingIn,

View file

@ -10,7 +10,8 @@ import { Row, Col, Alert, FormGroup } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor';
import { Bert } from 'meteor/themeteorchef:bert';
import { Helmet } from 'react-helmet';
import { Map, Circle } from 'react-leaflet';
import { Map, GeoJSON } from 'react-leaflet';
import { rectangleAround } from 'map-common-utils';
import Blaze from 'meteor/gadicc:blaze-react-component';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import NotFound from '/imports/ui/pages/NotFound/NotFound';
@ -115,8 +116,9 @@ class Fire extends React.Component {
t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) :
t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat });
}
return (fire && !loading ? (
const ready = fire && !loading;
const rect = ready ? rectangleAround({ lat: fire.lat, lon: fire.lon }, fire.track, fire.track) : null;
return (ready ? (
<div className="ViewFire">
<Helmet>
<title>{t('AppName')}: {t('Información adicional sobre fuego')}</title>
@ -137,14 +139,13 @@ class Fire extends React.Component {
zoom={13}
>
<Fragment>
<Circle
center={[fire.lat, fire.lon]}
color="red"
fillColor="red"
fillOpacity={0.0}
interactive={false}
radius={fire.scan ? fire.scan * 500 : 300}
<GeoJSON
ref={(circle) => { this.circle = circle; this.handleLeafletCircleLoad(circle); }}
data={rect}
color="red"
stroke
width="1"
fillOpacity="0.0"
/>
</Fragment>
<FireList

View file

@ -23,9 +23,11 @@ import 'leaflet-sleep/Leaflet.Sleep.js';
import geolocation from '/imports/startup/client/geolocation';
import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition';
import FireList from '/imports/ui/components/Maps/FireList';
import FireListUnion from '/imports/ui/components/Maps/FireListUnion';
import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
@ -82,19 +84,6 @@ class FiresMap extends React.Component {
});
}
/* shouldComponentUpdate(nextProps, nextState) {
* const notMoving = !nextState.moving;
* const markersChanged = this.state.useMarkers !== nextState.useMarkers;
* const unionChanged = this.state.showSubsUnion !== nextState.showSubsUnion;
* const otherViewport = this.state.viewport !== nextState.viewport;
* // const init = nextState.viewport.center === [0, 0];
* // console.log(notMoving ? 'Not moving map' : 'Moving map');
* // console.log(otherViewport ? 'Other viewport' : 'Not other viewport');
* console.log(`${otherViewport ? 'OTHER' : 'Not other'} viewport ${nextState.viewport.center} zoom: ${nextState.viewport.zoom}`);
* return this.state.init || (notMoving && otherViewport && this.state.moved) || unionChanged || markersChanged;
* }
*/
shouldComponentUpdate(nextProps, nextState) {
const notMoving = !nextState.moving;
return notMoving;
@ -202,6 +191,7 @@ class FiresMap extends React.Component {
if (Meteor.isDevelopment && this.props.activefires.length === 1) console.log(`Active fire: ${JSON.stringify(this.props.activefires[0])}`);
if (Meteor.isDevelopment) console.log(`Active fires total: ${this.props.activefires.length}`);
if (Meteor.isDevelopment) console.log(`Active fires union total: ${this.props.activefiresunion.length}`);
if (Meteor.isDevelopment) console.log(`False positives total: ${this.props.falsePositives.length}`);
if (Meteor.isDevelopment) console.log(`Fire alerts total: ${this.props.firealerts.length}`);
if (Meteor.isDevelopment) console.log(`Industries total: ${this.props.industries.length}`);
@ -246,7 +236,7 @@ class FiresMap extends React.Component {
<p className="firesmap-legend">
{ (this.props.activefires.length + this.props.firealerts.length) === 0 ?
<Fragment><Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment> :
<Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefires.length + this.props.firealerts.length }}</strong> fuegos activos. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment>
<Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefiresunion.length + this.props.firealerts.length }}</strong> fuegos activos. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment>
}
</p>
{isNotHomeAndMobile() && this.props.firealerts.length > 0 &&
@ -307,6 +297,14 @@ class FiresMap extends React.Component {
neighbour={false}
industries={false}
/>
{ Meteor.isDevelopment &&
<FireListUnion
t={t}
history={this.props.history}
firesUnion={this.props.activefiresunion}
nasa
/>
}
<FireList
t={t}
history={this.props.history}
@ -369,12 +367,14 @@ FiresMap.propTypes = {
userSubs: PropTypes.string,
userSubsBounds: PropTypes.string,
activefires: PropTypes.arrayOf(PropTypes.object).isRequired,
activefiresunion: PropTypes.arrayOf(PropTypes.object).isRequired,
lastFireDetected: PropTypes.object,
firealerts: PropTypes.arrayOf(PropTypes.object).isRequired,
falsePositives: PropTypes.arrayOf(PropTypes.object).isRequired,
industries: PropTypes.arrayOf(PropTypes.object).isRequired,
lastCheck: PropTypes.instanceOf(Date),
activefirestotal: PropTypes.number.isRequired,
activefiresuniontotal: PropTypes.number.isRequired,
center: PropTypes.arrayOf(PropTypes.number),
zoom: PropTypes.number,
marks: PropTypes.bool.isRequired,
@ -391,6 +391,7 @@ export default translate([], { wait: true })(withTracker(() => {
// console.log(`With industries: ${withIndustries}`);
let subscription;
let subscriptionUnion;
let alertSubscription;
const centerStored = store.get('firesmap_center');
@ -422,6 +423,14 @@ export default translate([], { wait: true })(withTracker(() => {
mapSize.get()[1].lat,
marks.get() && zoom.get() >= MAXZOOM
);
subscriptionUnion = Meteor.subscribe(
'activefiresunionmyloc',
mapSize.get()[0].lng,
mapSize.get()[0].lat,
mapSize.get()[1].lng,
mapSize.get()[1].lat,
false
);
alertSubscription = Meteor.subscribe(
'fireAlerts',
mapSize.get()[0].lng,
@ -442,6 +451,7 @@ export default translate([], { wait: true })(withTracker(() => {
});
Meteor.subscribe('activefirestotal');
Meteor.subscribe('activefiresuniontotal');
const settingsSubs = Meteor.subscribe('settings');
const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' });
@ -453,13 +463,15 @@ export default translate([], { wait: true })(withTracker(() => {
const lastFireDetected = ActiveFiresCollection.findOne({}, { sort: { when: -1 } });
return {
loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready()),
loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready() && subscriptionUnion.ready()),
userSubs: userSubs ? userSubs.value : null,
userSubsBounds: userSubs ? userSubsBounds.value : null,
subsready: settingsSubs.ready(),
// Not reactive query depending on zoom level
activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
activefiresunion: ActiveFiresUnion.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
activefirestotal: Counter.get('countActiveFires') + fireAlerts.length,
activefiresuniontotal: Counter.get('countActiveFiresUnion') + fireAlerts.length,
firealerts: fireAlerts,
falsePositives,
industries,

View file

@ -11,6 +11,7 @@ import PropTypes from 'prop-types';
// import { Link } from 'react-router-dom';
// https://www.npmjs.com/package/react-resize-detector
import ReactResizeDetector from 'react-resize-detector';
import MobileStoreButton from 'react-mobile-store-button';
import _ from 'lodash';
import 'html5-device-mockups/dist/device-mockups.min.css';
import 'bootstrap-carousel-swipe/carousel-swipe';
@ -20,7 +21,6 @@ import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap';
import ShareIt from '/imports/ui/components/ShareIt/ShareIt';
import getFallbackLang from '/imports/modules/lang-fallback';
import FiresMap from '../FiresMap/FiresMap';
import './Index.scss';
import './Index-custom.scss';
@ -102,6 +102,7 @@ class Index extends Component {
render() {
const { t } = this.props;
const title = `${t('AppName')}: ${t('Inicio')}`;
const androidUrl = 'https://play.google.com/store/apps/details?id=org.comunes.fires';
return (
<div className="IndexDisabled full-width">
<Helmet>
@ -181,6 +182,9 @@ class Index extends Component {
</div>
<p className="moto"><Trans>Siempre alerta a los fuegos en nuestro vecindario</Trans></p>
{/* <Link className="participe-btn btn btn-lg btn-warning" role="button" to="/#platforms">{this.props.t('Participa')}</Link> */}
<div className="android-btn">
<MobileStoreButton store="android" url={androidUrl} />
</div>
</div>
</section>
</section>

View file

@ -77,3 +77,7 @@
}
}
}
.android-btn > div {
height: 50px !important;
}

View file

@ -9,15 +9,17 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Bert } from 'meteor/themeteorchef:bert';
import { testId } from '/imports/ui/components/Utils/TestUtils';
import Col from '../../components/Col/Col';
import { withTracker } from 'meteor/react-meteor-data';
import InputHint from '../../components/InputHint/InputHint';
import validate from '../../../modules/validate';
import { translate } from 'react-i18next';
import { T9n } from 'meteor-accounts-t9n';
import { withTracker } from 'meteor/react-meteor-data';
import Col from '../../components/Col/Col';
import InputHint from '../../components/InputHint/InputHint';
import validate from '../../../modules/validate';
import './Profile.scss';
// List of languages: https://github.com/i18next/i18next/issues/1068
class Profile extends React.Component {
constructor(props) {
super(props);
@ -155,6 +157,7 @@ class Profile extends React.Component {
renderPasswordUser(loading, user) {
const { t, i18n } = this.props;
const enabledLangs = ['en', 'es', 'gl'];
const langName = {
en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català'
};
@ -202,7 +205,7 @@ class Profile extends React.Component {
{langName[i18n.language]}
</button>
<div className="dropdown-menu">
{i18n.languages.map(lang => (
{enabledLangs.map(lang => (
<button
className="dropdown-item"
onClick={() => this.onLangSelect(lang)}

View file

@ -63,8 +63,8 @@ class SubscriptionsMap extends React.Component {
handleLeafletLoad(map) {
// console.log('Map loading');
// console.log(map);
if (map) {
if (map && this.props.userSubs !== 'null') {
// console.log(`Union of ${this.props.userSubs}`);
this.state.union = subsUnion(this.state.union, {
map,
subs: this.props.userSubs,

2382
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "tcef",
"private": true,
"scripts": {
"start": "QT_QPA_PLATFORM='' MONGO_URL=mongodb://localhost:27017/fuegos meteor --settings settings-development.json",
"start": "QT_QPA_PLATFORM='' MONGO_URL=mongodb://fuegos:fuegos@localhost:27017/fuegos meteor --settings settings-development.json",
"test": "jest"
},
"dependencies": {
@ -10,8 +10,6 @@
"@cleverbeagle/dates": "^0.5.1",
"@cleverbeagle/seeder": "^1.3.1",
"@turf/buffer": "^5.1.5",
"@turf/truncate": "^5.1.5",
"@turf/union": "^5.1.5",
"babel-runtime": "^6.26.0",
"bcrypt": "^1.0.3",
"bootstrap-carousel-swipe": "0.0.6",
@ -40,19 +38,20 @@
"jsend": "^1.0.2",
"juice": "^4.2.2",
"leaflet": "^1.3.1",
"leaflet-geodesy": "^0.2.1",
"leaflet-graphicscale": "0.0.2",
"leaflet-headless": "^0.2.6",
"leaflet-sleep": "^0.5.1",
"lodash": "^4.17.4",
"loms.perlin": "^1.0.1",
"map-common-utils": "^0.5.0",
"maxmind": "^2.3.0",
"meteor-accounts-t9n": "^2.0.3",
"meteor-node-stubs": "^0.3.2",
"modernizr": "^3.6.0",
"moment": "^2.19.1",
"moment-timezone": "^0.5.14",
"node-gcm": "^1.0.2",
"node-geocoder": "^3.21.1",
"node-gyp": "^3.7.0",
"nodemailer": "^4.4.2",
"npm": "^5.8.0",
"pm2-master": "^1.1.3",
@ -74,15 +73,15 @@
"react-leaflet-fullscreen": "0.0.6",
"react-leaflet-google": "^3.2.1",
"react-leaflet-sidebarv2": "^0.5.1",
"react-mobile-store-button": "0.0.3",
"react-places-autocomplete": "^5.4.3",
"react-progressbar.js": "git://github.com/squarecat/react-progressbar.js.git",
"react-progress-bar.js": "^0.2.3",
"react-resize-detector": "^1.1.0",
"react-router-bootstrap": "^0.24.4",
"react-router-dom": "^4.2.2",
"react-router-hash-link": "^1.1.1",
"react-share": "^2.0.0",
"reactstrap": "^5.0.0-alpha.3",
"segfault-handler": "^1.0.0",
"simpl-schema": "^1.5.0",
"simple-line-icons": "^2.4.1",
"store": "^2.0.12",
@ -159,7 +158,8 @@
"_ensureIndex",
"_verifyEmailToken",
"_resetPasswordToken",
"_name"
"_name",
"_str"
]
}
],

@ -1 +1 @@
Subproject commit 3346b6db70c153b1dd66e6a16a0ad28f10ebf740
Subproject commit 4761da921f467838a2c01b34e84a7703ee49b12c

View file

@ -1,97 +1,156 @@
Your privacy is important to us.
# PRIVACY POLICY #
Comunes Association built the 'All Against the Fire' app and this SERVICE is provided by Comunes Association at no cost and is intended for use as is.
*Last updated July 12, 2018*
This page is used to inform website visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service.
**In Short**: *Your privacy is important to us. We do not do business with your data, we do not share it with third parties and we only worry about the fires and how to strengthen neighborhoods against them.*
If you choose to use our Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy.
Thank you for choosing to be part of our community 'All Against the Fires' at Comunes Association (“we”, “us”, or “our”). We are committed to protecting your personal information and your right to privacy. If you have any questions or concerns about our policy, or our practices with regards to your personal information, please contact us at info@comunes.org.
The terms used in this Privacy Policy have the same meanings as in our [Terms and Conditions](/terms), which is accessible at 'All Against the Fire' unless otherwise defined in this Privacy Policy.
When you visit our website [fires.comunes.org](https://fires.comunes.org), mobile application, and use our services, you trust us with your personal information. We take your privacy very seriously. In this privacy notice, we describe our privacy policy. We seek to explain to you in the clearest way possible what information we collect, how we use it and what rights you have in relation to it. We hope you take some time to read through it carefully, as it is important. If there are any terms in this privacy policy that you do not agree with, please discontinue use of our Sites or Apps and our services.
**Information Collection and Use**
This privacy policy applies to all information collected through our website (such as [fires.comunes.org](https://fires.comunes.org)), mobile application (the "**Apps**"), and/or any related services, or events (we refer to them collectively in this privacy policy as the "**Sites**").
For a better experience, while using our Service, we may require you to provide us with certain personally identifiable information, including but not limited to name, email, language, and the telegram username (of our Telegram users). The information that we request is will be retained by us and used as described in this privacy policy.
**Please read this privacy policy carefully as it will help you make informed decisions about sharing your personal information with us.**
In addition, the app may collect certain information automatically, including, but not limited to, the type of device you use, the IP address of your device, your mobile operating system, the type of Internet browsers you use, and information about the way you use the app.
We may use the information we collect in the following ways:
* To personalize your experience and to allow us to deliver the type of content in which you are most interested.
* To send periodic emails regarding our services.
## 1. WHAT INFORMATION DO WE COLLECT? ##
The app does use third party services that may collect information used to identify you.
### Information automatically collected ###
Links to privacy policy of third party service providers used by the app:
**In Short:** *Some information such as IP address and/or browser and device characteristics is collected
automatically when you visit our Sites or Apps.*
* [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Privacy_Policy/)
* [Google Maps](https://developers.google.com/maps/terms?hl=es#3-privacy-and-personal-information)
We automatically collect certain information when you visit, use or navigate the Sites or Apps. This information does not reveal your specific identity (like your name or contact information) but may include device and usage information, such as your IP address, browser and device characteristics, operating system, language preferences, referring URLs, device name, country, location, information about how and when you use our Sites or Apps and other technical information. This information is primarily needed to maintain the security and operation of our Sites or Apps, and for our internal analytics and reporting purposes. Like many businesses, we also collect information through cookies and similar technologies.
We have not enabled Google AdSense.
### Information collected through our Apps ###
**Does the app collect precise real time location information of the device?**
**In Short:** *We may collect information regarding your geo-location, push notifications, when you use our apps.*
When you visit the app, we may use GPS technology (or other similar technology) to determine your current location in order to determine the city you are located within and/or display a location map of local fires and/or provide alerts of future fires in your zone. We **will not** share your current location with other users or partners.
If you use our Apps, we may also collect the following information:
If you do not want us to use your location for the purposes set forth above, you should turn off the location services for the mobile app located in your account settings or in your mobile phone settings and/or within the app and/or when asked by your browser.
- *Geo-Location Information.* We may request at some point access or permission to location-based information from your mobile device to provide location-based services. If you wish to change our access or permissions, you may do so in your device's settings.
- *Push Notifications.* We may request to send you push notifications regarding your account or the mobile application. If you wish to opt-out from receiving these types of communications, you may turn them off in your device's settings.
**How do we protect your information?**
We do not use vulnerability scanning and/or scanning to PCI standards. We only provide articles and information. We never ask for credit card numbers. We do not use Malware Scanning.
## 2. HOW DO WE USE YOUR INFORMATION? ##
Your personal information is contained behind secured networks and is only accessible by a limited number of persons who have special access rights to such systems, and are required to keep the information confidential. In addition, all sensitive information you supply is encrypted via Secure Socket Layer (SSL) technology.
**In Short:** *We process your information for purposes based on legitimate interests, the fulfillment of our contract with you, compliance with our legal obligations, and/or your consent.*
We implement a variety of security measures when a user enters, submits, or accesses their information to maintain the safety of your personal information.
We use personal information collected via our Sites or Apps for a variety of purposes described below. We process your personal information for these purposes in order to enter into or perform a contract with you ("Contractual"), with your consent ("Consent"), and/or for compliance with our legal obligations ("Legal Reasons"). We indicate the specific processing grounds we rely on next to each purpose listed below.
**Log Data**
We use the information we collect or receive:
We want to inform you that whenever you use our Service, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our Service, the time and date of your use of the Service, and other statistics.
- **To facilitate account creation and logon process** with your Consent. If you choose to link your account with us to a third party account \*(such as your Google or Telegram account), we use the information you allowed us to collect from those third parties to facilitate account creation and logon process.
- **Request Feedback**. We may use your information to request feedback and to contact you about your use of our Sites or Apps.
- **To enable user-to-user communications** with your Consent. We may use your information in order to enable user-to-user communications with each user's consent.
- **To enforce our terms, conditions and policies**.
- **To respond to legal requests and prevent harm** for Legal Reasons. If we receive a subpoena or other legal request, we may need to inspect the data we hold to determine how to respond.
**Do we use 'cookies'?**
Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow) that enables the site's or service provider's systems to recognize your browser and capture and remember certain information. For instance, we use cookies to help us remember and process your prefered language. They are also used to help us understand your preferences based on previous or current site activity, which enables us to provide you with improved services. We also use cookies to help us compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future.
## 3. WILL YOUR INFORMATION BE SHARED WITH ANYONE? ##
We use cookies to understand and save user's preferences for future visits.
**In Short:** *We only share information with your consent, to comply with laws or to protect your rights.*
You can choose to have your computer warn you each time a cookie is being sent, or you can choose to turn off all cookies. You do this through your browser settings. Since browser is a little different, look at your browser's Help Menu to learn the correct way to modify your cookies.
We only share and disclose your information in the following situations:
If you turn cookies off, some of the features that make your site experience more efficient may not function properly.
- **Compliance with Laws**. We may disclose your information where we are legally required to do so in order to comply with applicable law, governmental requests, a judicial proceeding, court order, or legal process, such as in response to a court order or a subpoena (including in response to public authorities to meet national security or law enforcement requirements).
- **Vital Interests and Legal Rights**. We may disclose your information where we believe it is necessary to investigate, prevent, or take action regarding potential violations of our policies, suspected fraud, situations involving potential threats to the safety of any person and illegal activities, or as evidence in litigation in which we are involved.
- **With your Consent**. We may disclose your personal information for any other purpose with your consent.
**Service Providers**
We may employ third-party companies and individuals to facilitate our Service.
## 4. DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES? ##
**Third-party disclosure**
**In Short:** *We may use cookies and other tracking technologies to collect and store your information.*
We do not sell, trade, or otherwise transfer to outside parties your Personally Identifiable Information.
We may use cookies to access or store information about account login and your user preferences (like your prefered language).
**Security**
We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
## 5. DO WE USE GOOGLE MAPS? ##
**Links to Other Sites**
**In Short:** *Yes, we use Google Maps for the purpose of providing better service.*
We do not include or offer third-party products or services on our website but this Service may contain links to other sites in user comments. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
This website, mobile application, or Telegram application uses Google Maps APIs. You may find the Google Maps APIs Terms of Service [here](https://developers.google.com/maps/terms). To better understand Google's Privacy Policy, please refer to this [link](https://policies.google.com/privacy).
**Childrens Privacy**
By using our Maps API Implementation, you agree to be bound by Google's Terms of Service. You agree to allow us to obtain or cache your location. You may revoke your consent at anytime. We use information about location in conjunction with data from other data providers.
These Services do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
**Fair Information Practices**
## 6. HOW LONG DO WE KEEP YOUR INFORMATION? ##
The Fair Information Practices Principles form the backbone of privacy law in the United States and the concepts they include have played a significant role in the development of data protection laws around the globe. Understanding the Fair Information Practice Principles and how they should be implemented is critical to comply with the various privacy laws that protect personal information.
**In Short:** *We keep your information for as long as necessary to fulfill the purposes outlined in this privacy policy unless otherwise required by law.*
In order to be in line with Fair Information Practices we will take the following responsive action, should a data breach occur:
* We will notify you via email, within 7 business days
We will only keep your personal information for as long as it is necessary for the purposes set out in this privacy policy, unless a longer retention period is required or permitted by law (such as tax, accounting or other legal requirements). No purpose in this policy will require us keeping your personal information for longer than the period of time in which users have an account with us.
We also agree to the Individual Redress Principle which requires that individuals have the right to legally pursue enforceable rights against data collectors and processors who fail to adhere to the law. This principle requires not only that individuals have enforceable rights against data users, but also that individuals have recourse to courts or government agencies to investigate and/or prosecute non-compliance by data processors.
When we have no ongoing legitimate need to process your personal information, we will either delete or anonymize it, or, if this is not possible (for example, because your personal information has been stored in backup archives), then we will securely store your personal information and isolate it from any further processing until deletion is possible.
**Changes to This Privacy Policy**
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
## 7.HOW DO WE KEEP YOUR INFORMATION SAFE? ##
**Your Consent**
**In Short:** *We aim to protect your personal information through a system of organizational and technical security measures.*
By using the app, you are consenting to our processing of your information as set forth in this Privacy Policy now and as amended by us. "Processing," means using cookies on a computer/hand held device or using or touching information in any way, including, but not limited to, collecting, storing, deleting, using, combining and disclosing information, all of which activities will take place in Spain. If you reside outside Spain your information will be transferred, processed and stored there under Spain privacy standards.
We have implemented appropriate technical and organizational security measures designed to protect the security of any personal information we process. However, please also remember that we cannot guarantee that the internet itself is 100% secure. Although we will do our best to protect your personal information, transmission of personal information to and from our Sites or Apps is at your own risk. You should only access the services within a secure environment.
**Contact Us**
If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us: info AT comunes DOT org.
## 8. DO WE COLLECT INFORMATION FROM MINORS? ##
**In Short:** *We do not knowingly collect data from or market to children under 18 years of age.*
We do not knowingly solicit data from or market to children under 18 years of age. By using the Sites or Apps, you represent that you are at least 18 or that you are the parent or guardian of such a minor and consent to such minor dependent's use of the Sites or Apps. If we learn that personal information from users less than 18 years of age has been collected, we will deactivate the account and take reasonable measures to promptly delete such data from our records. If you become aware of any data we have collected from children under age 18, please contact us at info@comunes.org.
## 9. WHAT ARE YOUR PRIVACY RIGHTS? ##
**In Short:** *In some regions, such as the European Economic Area, you have rights that allow you greater access to and control over your personal information. You may review, change, or terminate your account at any time.*
In some regions (like the European Economic Area), you have certain rights under applicable data protection laws. These may include the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or erasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In certain circumstances, you may also have the right to object to the processing of your personal information. To make such a request, please use the contact details provided below. We will consider and act upon any request in accordance with applicable data protection laws.
If we are relying on your consent to process your personal information, you have the right to withdraw your consent at any time. Please note however that this will not affect the lawfulness of the processing before its withdrawal.
If you are resident in the European Economic Area and you believe we are unlawfully processing your personal information, you also have the right to complain to your local data protection supervisory authority. You can find their contact details here: [http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm](http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm)
### Account Information ###
If you would at any time like to review or change the information in your account or terminate your account, you can:
- Log into your account settings and update your user account.
Upon your request to terminate your account, we will deactivate or delete your account and information from our active databases. However, some information may be retained in our files to prevent fraud, troubleshoot problems, assist with any investigations, enforce our Terms of Use and/or comply with legal requirements.
**Cookies and similar technologies:** Most Web browsers are set to accept cookies by default. If you prefer, you can usually choose to set your browser to remove cookies and to reject cookies. If you choose to remove cookies or reject cookies, this could affect certain features or services of our Sites or Apps.
**Opting out of email notifications:** You can unsubscribe from our notification emails at any time by clicking on the unsubscribe link in the emails that we send or by contacting us using the details provided below. You will then be removed from these emails however, we will still need to send you service-related emails that are necessary for the administration and use of your account. To otherwise opt-out, you may:
- Note your preferences when you register an account with the site.
- Access your account settings and update preferences.
## 10. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? ##
**In Short:** *Yes, if you are a resident of California, you are granted specific rights regarding access to your personal information.*
California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users who are California residents to request and obtain from us, once a year and free of charge, information about categories of personal information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all third parties with which we shared personal information in the immediately preceding calendar year. If you are a California resident and would like to make such a request, please submit your request in writing to us using the contact information provided below.
If you are under 18 years of age, reside in California, and have a registered account with the Sites or Apps, you have the right to request removal of unwanted data that you publicly post on the Sites or Apps. To request removal of such data, please contact us using the contact information provided below, and include the email address associated with your account and a statement that you reside in California. We will make sure the data is not publicly displayed on the Sites or Apps, but please be aware that the data may not be completely or comprehensively removed from our systems.
## 11. DO WE MAKE UPDATES TO THIS POLICY? ##
**In Short:** *Yes, we will update this policy as necessary to stay compliant with relevant laws.*
We may update this privacy policy from time to time. The updated version will be indicated by an updated "Revised" date and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy policy, we may notify you either by prominently posting a notice of such changes or by directly sending you a notification. We encourage you to review this privacy policy frequently to be informed of how we are protecting your information.
## 12. HOW CAN YOU CONTACT US ABOUT THIS POLICY? ##
If you have questions or comments about this policy, you may email us at info@comunes.org or by post to:
ASOCIACIÓN COMUNES
C/ COLÓN 21, 5º DCHA, ESC DCHA
03001 ALICANTE
SPAIN
If you have any further questions or comments about us or our policies, email us at info@comunes.org or by post to:
ASOCIACIÓN COMUNES
C/ COLÓN 21, 5º DCHA, ESC DCHA
03001 ALICANTE
SPAIN

View file

@ -1,97 +1,155 @@
Su privacidad es importante para nosotros.
# POLÍTICA DE PRIVACIDAD #
La Asociación Comunes ha creado esta aplicación 'Tod@s contra el fuego' y este SERVICIO que se provee por la Asociación de Comunes sin costo y está destinado a ser usado tal cual.
*Última actualización 12 de julio de 2018*
Esta página se utiliza para informar a los visitantes del sitio web sobre nuestras políticas con la recopilación, el uso y la divulgación de información personal si alguien decide utilizar nuestro Servicio.
**En resumen**: *Su privacidad es importante para nosotros/as. No hacemos negocios con sus datos, no los compartimos con terceros y solo nos preocupan los incendios y cómo fortalecer vecindarios contra ellos.*
Si elige usar nuestro Servicio, acepta la recopilación y el uso de la información en relación con esta política. La información personal que recopilamos se usa para proporcionar y mejorar el servicio. No usaremos ni compartiremos su información con nadie, excepto según se describe en esta Política de privacidad.
Gracias por elegir ser parte de nuestra comunidad 'Tod@s contra los Fuegos' en la Asociación de Comunes ("nosotras/os" o "nos"). Nos comprometemos a proteger su información personal y su derecho a la privacidad. Si tiene alguna pregunta o inquietud sobre nuestra política o nuestras prácticas con respecto a su información personal, contáctenos a info@comunes.org.
Los términos utilizados en esta Política de privacidad tienen los mismos significados que en nuestros [Términos y condiciones](/terms), a los que se puede acceder en "Tod@s contra el fuego" a menos que se defina lo contrario en esta Política de privacidad.
Cuando visita nuestro sitio web [fuegos.comunes.org](https://fuegos.comunes.org), la aplicación móvil y utiliza nuestros servicios, confía en nosotras/os con su información personal. Nos tomamos su privacidad muy en serio. En este aviso de privacidad, describimos nuestra política de privacidad. Buscamos explicarte de la manera más clara posible qué información recopilamos, cómo la utilizamos y qué derechos tienes en relación con ella. Esperamos que se tome un tiempo para leer detenidamente, ya que es importante. Si hay términos en esta política de privacidad con los que no está de acuerdo, interrumpa el uso de nuestros Sitios o Aplicaciones y nuestros servicios.
**Recopilación y uso de información**
Esta política de privacidad se aplica a toda la información recopilada a través de nuestro sitio web (como [fuegos.comunes.org](https://fuegos.comunes.org)), la aplicación móvil (las "**Aplicaciones**") y/o cualquier servicio relacionado o eventos (nos referimos a ellos colectivamente en esta política de privacidad como los "**Sitios**").
Para una mejor experiencia, al utilizar nuestro Servicio, podemos solicitarle que nos brinde cierta información de identificación personal, que incluye, entre otros, nombre, correo electrónico, idioma y el nombre de usuario del telegrama (de nuestros usuarios de Telegram). La información que solicitamos será retenida por nosotros y utilizada como se describe en esta política de privacidad.
**Lea atentamente esta política de privacidad, ya que le ayudará a tomar decisiones informadas sobre cómo compartir su información personal con nosotras/os.**
Además, la aplicación puede recopilar cierta información automáticamente, que incluye, entre otros, el tipo de dispositivo que usa, la dirección IP de su dispositivo, su sistema operativo móvil, el tipo de navegador de Internet que utiliza e información sobre la forma que usas la aplicación.
Podemos usar la información que recopilamos de las siguientes maneras:
* Para personalizar su experiencia y permitirnos ofrecer el tipo de contenido que más le interesa.
* Para enviar correos electrónicos periódicos con respecto a nuestros servicios.
## 1. ¿QUÉ INFORMACIÓN RECOPILAMOS? ##
La aplicación utiliza servicios de terceros que pueden recopilar información utilizada para identificarlo.
### Información recopilada automáticamente ###
Enlaces a la política de privacidad de proveedores de servicios de terceros utilizados por la aplicación:
**En resumen:** *Recopilamos información automáticamente, como la dirección IP y/o las características del navegador y del dispositivo, cuando visita nuestros Sitios o Aplicaciones.*
* [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Privacy_Policy/)
* [Google Maps](https://developers.google.com/maps/terms?hl=es#3-privacy-and-personal-information)
Recopilamos cierta información automáticamente cuando visita, utiliza o navega por nuestros Sitios o Aplicaciones. Esta información no revela su identidad específica (como su nombre o información de contacto) pero puede incluir información de uso y dispositivo, como su dirección IP, características del navegador y del dispositivo, sistema operativo, preferencias de idioma, URL de referencia, nombre del dispositivo, país, ubicación , información sobre cómo y cuándo utiliza nuestros sitios o aplicaciones y otra información técnica. Esta información es principalmente necesaria para mantener la seguridad y el funcionamiento de nuestros sitios o aplicaciones, y para nuestros fines de análisis internos e informes. Al igual que muchas empresas, también recopilamos información a través de cookies y tecnologías similares.
No hemos habilitado Google AdSense.
### Información recopilada a través de nuestras aplicaciones ###
**¿La aplicación recopila información precisa sobre la ubicación en tiempo real del dispositivo?**
**En resumen:** *Podemos recopilar información sobre su ubicación geográfica, notificaciones push, cuando usa nuestras aplicaciones.*
Cuando visita la aplicación, podemos usar tecnología GPS (u otra tecnología similar) para determinar su ubicación actual a fin de determinar la ciudad en la que se encuentra y/o mostrar un mapa de ubicación de incendios locales y/o proporcionar alertas de incendios futuros en tu zona. **No** compartiremos su ubicación actual con otros usuarios o socios.
Si usa nuestras aplicaciones, también podemos recopilar la siguiente información:
Si no desea que usemos su ubicación para los fines establecidos anteriormente, debe desactivar los servicios de ubicación para la aplicación móvil ubicados en la configuración de su cuenta o en la configuración de su teléfono móvil y/o dentro de la aplicación y/o cuando se le solicite por tu navegador.
- *Información de geolocalización.* Podemos solicitar en algún momento acceso o permiso a la información basada en la ubicación desde su dispositivo móvil para proporcionar servicios basados en la ubicación. Si desea cambiar nuestro acceso o permisos, puede hacerlo en la configuración de su dispositivo.
- *Notificaciones Push.* Podemos solicitar el envío de notificaciones push con respecto a su cuenta o la aplicación móvil. Si desea optar por no recibir este tipo de comunicaciones, puede desactivarlas en la configuración de su dispositivo.
**¿Cómo protegemos tu información?**
No utilizamos el escaneo y/o escaneo de vulnerabilidades a estándares PCI. Solo proporcionamos artículos e información. Nunca pedimos números de tarjetas de crédito. No utilizamos el Malware Scanning.
## 2. ¿CÓMO USAMOS SU INFORMACIÓN? ##
Su información personal está almacenada detrás de redes seguras y solo es accesible para un número limitado de personas que tienen derechos especiales de acceso a dichos sistemas, y se les exige mantener la confidencialidad de la información. Además, toda la información sensible que usted suministra se cifra a través de la tecnología Secure Socket Layer (SSL).
**En resumen:** *Procesamos su información para fines basados en intereses legítimos, el cumplimiento de nuestro contrato con usted, el cumplimiento de nuestras obligaciones legales y/o su consentimiento.*
Implementamos una variedad de medidas de seguridad cuando un usuario ingresa, envía o accede a su información para mantener la seguridad de su información personal.
Usamos información personal recopilada a través de nuestros Sitios o Aplicaciones para una variedad de propósitos que se describen a continuación. Procesamos su información personal para estos fines a fin de celebrar o realizar un contrato con usted ("Contractual"), con su consentimiento ("Consentimiento"), y/o para cumplir con nuestras obligaciones legales ("Razones legales"). Indicamos los motivos de procesamiento específicos en los que confiamos junto a cada uno de los propósitos enumerados a continuación.
**Dato de registro**
Usamos la información que recopilamos o recibimos:
Queremos informarle que cada vez que utiliza nuestro Servicio, en caso de error en la aplicación, recopilamos datos e información (a través de productos de terceros) en su teléfono, llamados Datos de registro. Este registro de datos puede incluir información como la dirección del protocolo de Internet ("IP"), el nombre del dispositivo, la versión del sistema operativo, la configuración de la aplicación al utilizar nuestro servicio, la hora y fecha de uso del servicio y otras estadísticas.
- **Para facilitar la creación de la cuenta y el proceso de inicio de sesión** con su consentimiento. Si elige vincular su cuenta con nosotras/os a una cuenta de terceros \*(como su cuenta de Google o Telegram), usamos la información que nos permitió recopilar de esos terceros para facilitar la creación de la cuenta y el proceso de inicio de sesión.
- **Solicitud de comentarios**. Podemos utilizar su información para solicitar comentarios y para comunicarnos con usted sobre su uso de nuestros Sitios o Aplicaciones.
- **Para habilitar las comunicaciones de usuario a usuario** con su consentimiento. Podemos usar su información para permitir las comunicaciones de usuario a usuario con el consentimiento de cada usuario.
- **Para hacer cumplir nuestros términos, condiciones y políticas**.
- **Para responder a solicitudes legales y evitar daños** por razones legales. Si recibimos una citación u otra solicitud legal, es posible que tengamos que inspeccionar los datos que tenemos para determinar cómo responder.
**¿Usamos 'cookies'?**
Sí. Las cookies son pequeños archivos que un sitio o su proveedor de servicios transfiere al disco duro de su computadora a través de su navegador web (si lo permite) que permite que los sistemas del sitio o del proveedor de servicios reconozcan su navegador y capturen y recuerden cierta información. Por ejemplo, usamos cookies para ayudarnos a recordar y procesar su idioma preferido. También se utilizan para ayudarnos a comprender sus preferencias en función de la actividad del sitio anterior o actual, lo que nos permite brindarle mejores servicios. También utilizamos cookies para ayudarnos a recopilar datos agregados sobre el tráfico del sitio y la interacción del sitio para que podamos ofrecer mejores experiencias y herramientas del sitio en el futuro.
## 3. ¿SU INFORMACIÓN SERÁ COMPARTIDA CON ALGUIEN? ##
Usamos cookies para comprender y guardar las preferencias del usuario para futuras visitas.
**En resumen:** *Solo compartimos información con su consentimiento, para cumplir con las leyes o proteger sus derechos*
Puede optar por que su computadora lo advierta cada vez que se envía una cookie, o puede optar por desactivar todas las cookies. Lo haces a través de la configuración de tu navegador. Dado que el navegador es un poco diferente, consulte el Menú de Ayuda de su navegador para conocer la forma correcta de modificar sus cookies.
Solo compartimos y divulgamos su información en las siguientes situaciones:
Si desactiva las cookies, es posible que algunas de las funciones que hacen que su sitio sea más eficiente no funcionen correctamente.
- **De acuerdo con las leyes**. Podemos divulgar su información cuando se nos requiera legalmente para cumplir con la ley aplicable, las solicitudes gubernamentales, un procedimiento judicial, una orden judicial o un proceso legal, como por ejemplo en respuesta a una orden judicial o una citación judicial (incluso en respuesta a las autoridades públicas para cumplir con los requisitos de seguridad nacional o cumplimiento de la ley).
- **Intereses vitales y derechos legales**. Podemos divulgar su información cuando creemos que es necesario investigar, prevenir o tomar medidas con respecto a posibles violaciones de nuestras políticas, sospecha de fraude, situaciones que involucren amenazas potenciales a la seguridad de cualquier persona y actividades ilegales, o como evidencia en un litigio en el cual estamos involucrados.
- **Con su consentimiento**. Podemos divulgar su información personal para cualquier otro propósito con su consentimiento.
**Proveedores de servicio**
Podemos emplear compañías e individuos de terceros para facilitar nuestro Servicio.
## 4. ¿USAMOS COOKIES Y OTRAS TECNOLOGÍAS DE SEGUIMIENTO? ##
**Divulgación de terceros**
**En resumen:** *Podemos utilizar cookies y otras tecnologías de seguimiento para recopilar y almacenar su información.*
No vendemos, comercializamos ni transferimos a terceros su información de identificación personal.
Podemos usar cookies para acceder o almacenar información sobre el inicio de sesión de la cuenta y sus preferencias de usuario (como su idioma preferido).
**Seguridad**
Valoramos su confianza al proporcionarnos su información personal, por lo tanto, nos esforzamos por utilizar medios comercialmente aceptables para protegerla. Pero recuerde que ningún método de transmisión a través de Internet o método de almacenamiento electrónico es 100% seguro y confiable, y no podemos garantizar su seguridad absoluta.
## 5. ¿USAMOS GOOGLE MAPS? ##
**Enlaces a otros sitios**
**En resumen:** *Sí, utilizamos Google Maps con el fin de proporcionar un mejor servicio.*
No incluimos ni ofrecemos productos o servicios de terceros en nuestro sitio web, pero este Servicio puede contener enlaces a otros sitios en los comentarios de los usuarios. Si hace clic en un enlace de un tercero, se lo dirigirá a ese sitio. Tenga en cuenta que estos sitios externos no son operados por nosotros. Por lo tanto, le recomendamos encarecidamente que revise la Política de privacidad de estos sitios web. No tenemos control ni asumimos ninguna responsabilidad por el contenido, las políticas de privacidad o las prácticas de sitios o servicios de terceros.
Este sitio web, aplicación móvil o aplicación Telegram utiliza las API de Google Maps. Puede encontrar las Condiciones del servicio de las API de Google Maps [aquí](https://developers.google.com/maps/terms). Para comprender mejor la Política de privacidad de Google, consulte este [enlace](https://policies.google.com/privacy).
**Privacidad de los niños**
Al utilizar nuestra implementación de Maps API, usted acepta estar sujeto a los Términos de Servicio de Google. Usted acepta permitirnos obtener o almacenar en caché su ubicación. Puede revocar su consentimiento en cualquier momento. Usamos información sobre la ubicación junto con datos de otros proveedores de datos.
Estos servicios no se dirigen a personas menores de 13 años. No recopilamos deliberadamente información personal identificable de niños menores de 13 años. En el caso de que descubramos que un niño menor de 13 años nos ha proporcionado información personal, la borramos inmediatamente de nuestros servidores. Si usted es un padre o tutor y sabe que su hijo nos ha proporcionado información personal, comuníquese con nosotros para que podamos hacer las acciones necesarias.
**Prácticas justas de información**
## 6. ¿CUÁNTO TIEMPO CONSERVAMOS SU INFORMACIÓN? ##
Los principios de prácticas justas de información (del inglés 'Fair Information Practices') forman la columna vertebral de la ley de privacidad en los Estados Unidos y los conceptos que incluyen han jugado un papel importante en el desarrollo de las leyes de protección de datos en todo el mundo. Comprender los principios prácticos de información justa y cómo deben implementarse es fundamental para cumplir con las diversas leyes de privacidad que protegen la información personal.
**En resumen:** *Conservamos su información el tiempo que sea necesario para cumplir con los fines descritos en esta política de privacidad, a menos que la ley así lo requiera.*
Para estar en línea con las prácticas de información justas, tomaremos la siguiente medida de respuesta, en caso de que se produzca una violación de datos:
* Le notificaremos por correo electrónico dentro de los 7 días hábiles
Solo conservaremos su información personal durante el tiempo que sea necesario para los fines establecidos en esta política de privacidad, a menos que la ley exija o permita un período de retención más largo (como impuestos, contabilidad u otros requisitos legales). Ningún propósito de esta política nos obligará a conservar su información personal por un período mayor al que el usuario tenga una cuenta con nosotras/os.
También aceptamos el principio de reparación individual que exige que las personas tengan derecho a buscar legalmente derechos exigibles contra los recolectores de datos y procesadores que no cumplan con la ley. Este principio requiere no solo que los individuos tengan derechos exigibles contra los usuarios de los datos, sino también que los individuos recurran a los tribunales o agencias gubernamentales para investigar y/o enjuiciar el incumplimiento por parte de los procesadores de datos.
Cuando no tenemos una necesidad legítima continua de procesar su información personal, la borraremos o la anonimizamos, o, si esto no es posible (por ejemplo, porque su información personal ha sido almacenada en archivos de respaldo), almacenaremos de manera segura su información personal y aislarlo de cualquier procesamiento posterior hasta que la eliminación sea posible.
**Cambios a esta política de privacidad**
Es posible que actualicemos nuestra política de privacidad de vez en cuando. Por lo tanto, se recomienda revisar esta página periódicamente para cualquier cambio. Le notificaremos de cualquier cambio publicando la nueva política de privacidad en esta página. Estos cambios entran en vigencia inmediatamente después de que se publiquen en esta página.
## 7. ¿CÓMO MANTENEMOS SU INFORMACIÓN SEGURA? ##
**Tu consentimiento**
**En resumen:** *Nuestro objetivo es proteger su información personal a través de un sistema de medidas de seguridad técnicas y organizativas.*
Al utilizar la aplicación, usted está dando su consentimiento para que procesemos su información según lo establecido en esta política de privacidad ahora y en la forma en que nosotros lo modifiquemos. "Procesamiento" significa usar cookies en una computadora/dispositivo de mano o usar o tocar información de cualquier manera, incluyendo, pero no limitado a, recolectar, almacenar, eliminar, usar, combinar y divulgar información, todas las cuales se llevarán a cabo en España. Si reside fuera de España, su información será transferida, procesada y almacenada allí según los estándares de privacidad de España.
Hemos implementado medidas de seguridad organizacionales y técnicas adecuadas diseñadas para proteger la seguridad de cualquier información personal que procesamos. Sin embargo, recuerde que no podemos garantizar que Internet sea 100% seguro. Aunque haremos nuestro mejor esfuerzo para proteger su información personal, la transmisión de información personal hacia y desde nuestros Sitios o Aplicaciones es bajo su propio riesgo. Solo debe acceder a los servicios dentro de un entorno seguro.
**Contáctenos**
Si tiene alguna pregunta o sugerencia sobre nuestra Política de privacidad, no dude en ponerse en contacto con nosotros: info ARROBA comunes PUNTO org.
## 8. ¿RECOPILAMOS INFORMACIÓN DE MENORES DE EDAD? ##
**En resumen:** *No recopilamos deliberadamente ni comercializamos datos de niños menores de 18 años.*
No solicitamos intencionalmente ni comercializamos datos de niños menores de 18 años. Al utilizar los Sitios o Aplicaciones, usted declara que tiene al menos 18 años o que es el padre o tutor de dicho menor y acepta el uso de los Sitios o Aplicaciones por parte de dicho menor dependiente. Si descubrimos que se ha recopilado información personal de usuarios menores de 18 años, desactivaremos la cuenta y tomaremos medidas razonables para eliminar dichos datos de inmediato de nuestros registros. Si toma conocimiento de cualquier información que hayamos recopilado de niños menores de 18 años, contáctenos a info@comunes.org.
## 9. ¿CUÁLES SON SUS DERECHOS DE PRIVACIDAD? ##
**En resumen:** *En algunas regiones, como el Unión Europea, tiene derechos que le permiten un mayor acceso y control sobre su información personal. Puede revisar, cambiar o cancelar su cuenta en cualquier momento.*
En algunas regiones (como la Unión Europea), usted tiene ciertos derechos bajo las leyes de protección de datos aplicables. Estos pueden incluir el derecho (i) de solicitar acceso y obtener una copia de su información personal, (ii) solicitar una rectificación o cancelación; (iii) restringir el procesamiento de su información personal; y (iv) si corresponde, a la portabilidad de datos. En determinadas circunstancias, también puede tener derecho a oponerse al procesamiento de su información personal. Para realizar dicha solicitud, utilice los datos de contacto que se proporcionan a continuación. Consideraremos y actuaremos en cualquier solicitud de acuerdo con las leyes de protección de datos aplicables.
Si confiamos en su consentimiento para procesar su información personal, tiene derecho a retirar su consentimiento en cualquier momento. Sin embargo, tenga en cuenta que esto no afectará la legalidad del procesamiento antes de su retirada.
Si usted reside en la Unión Europea y considera que estamos procesando ilegalmente su información personal, también tiene derecho a presentar una queja ante la autoridad de supervisión de protección de datos local. Puede encontrar sus datos de contacto aquí: [http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm](http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm)
### Información de la cuenta ###
Si en algún momento desea revisar o cambiar la información en su cuenta o cancelar su cuenta, puede:
- Inicie sesión en la configuración de su cuenta y actualice su cuenta de usuario.
Tras su solicitud para cancelar su cuenta, desactivaremos o eliminaremos su cuenta y la información de nuestras bases de datos activas. Sin embargo, parte de la información se puede conservar en nuestros archivos para evitar fraudes, solucionar problemas, ayudar con cualquier investigación, hacer cumplir nuestros Términos de uso y/o cumplir con los requisitos legales.
**Cookies y tecnologías similares:** La mayoría de los navegadores web están configurados para aceptar cookies por defecto. Si lo prefiere, generalmente puede elegir configurar su navegador para que elimine las cookies y rechace las cookies. Si elige eliminar las cookies o rechazar las cookies, esto podría afectar ciertas funciones o servicios de nuestros sitios o aplicaciones.
**Optar por no recibir notificaciones por correo electrónico:** Puede darse de baja de nuestros correos electrónicos de notificación en cualquier momento haciendo clic en el enlace para darse de baja en los correos electrónicos que le enviamos o poniéndose en contacto con nosotras/os utilizando los detalles que se proporcionan a continuación. A continuación, se le eliminará de estos correos electrónicos; sin embargo, aún así le enviaremos correos electrónicos relacionados con el servicio que son necesarios para la administración y el uso de su cuenta. Para optar por no participar, usted puede:
- Tener en cuenta sus preferencias cuando registra una cuenta en el sitio.
- Acceder a la configuración de tu cuenta y actualizar tus preferencias.
## 10. ¿LOS RESIDENTES DE CALIFORNIA TIENEN DERECHOS DE PRIVACIDAD ESPECÍFICOS? ##
**En resumen:** *Sí, si usted es residente de California, se le otorgan derechos específicos con respecto al acceso a su información personal.*
La Sección 1798.83 del Código Civil de California, también conocida como la ley "Shine The Light", permite a nuestros usuarios que son residentes de California solicitar y obtener de nosotras/os, una vez al año y sin cargo, información sobre categorías de información personal (si corresponde) que divulgado a terceros para fines de marketing directo y los nombres y direcciones de todos los terceros con quienes compartimos información personal en el año calendario inmediatamente anterior. Si usted es residente de California y desea realizar dicha solicitud, envíe su solicitud por escrito a nosotras/os utilizando la información de contacto que se proporciona a continuación.
Si eres menor de 18 años, resides en California y tienes una cuenta registrada en los Sitios o Aplicaciones, tienes el derecho de solicitar la eliminación de datos no deseados que publiques en los Sitios o Aplicaciones. Para solicitar la eliminación de dichos datos, contáctenos utilizando la información de contacto que se proporciona a continuación, e incluya la dirección de correo electrónico asociada a su cuenta y una declaración de que reside en California. Nos aseguraremos de que los datos no se muestren públicamente en los Sitios o Aplicaciones, pero tenga en cuenta que es posible que los datos no se eliminen completa o completamente de nuestros sistemas.
## 11. ¿HACEMOS ACTUALIZACIONES DE ESTA POLÍTICA? ##
**En resumen:** *Sí, actualizaremos esta política según sea necesario para cumplir con las leyes pertinentes.*
Podemos actualizar esta política de privacidad de vez en cuando. La versión actualizada se indicará con una fecha actualizada "Revisada" y la versión actualizada entrará en vigencia tan pronto como sea accesible. Si realizamos cambios sustanciales a esta política de privacidad, podemos notificarlo publicando de manera prominente un aviso de dichos cambios o enviándole directamente una notificación. Le recomendamos que revise esta política de privacidad con frecuencia para recibir información sobre cómo protegemos su información.
## 12. ¿CÓMO PUEDE CONTACTARNOS SOBRE ESTA POLÍTICA? ##
Si tiene preguntas o comentarios sobre esta política, puede enviarnos un correo electrónico a info@comunes.org o por correo postal a:
ASOCIACIÓN COMUNES
C/ COLÓN 21, 5º DCHA, ESC DCHA
03001 ALICANTE
ESPAÑA
Si tiene más preguntas o comentarios sobre nosotras/os o nuestras políticas, envíenos un correo electrónico a info@comunes.org o por correo postal a:
ASOCIACIÓN COMUNES
C/ COLÓN 21, 5º DCHA, ESC DCHA
03001 ALICANTE
ESPAÑA

View file

@ -250,5 +250,6 @@
"Hay más información sobre un fuego":
"There is more information about a fire",
"esDecirTalDia": "That is on {{date}}",
"Actualizando...": "Updating..."
"Actualizando...": "Updating...",
"Alerta de fuego": "Alert of fire"
}

View file

@ -335,5 +335,6 @@
"Hay más información sobre un fuego":
"Hay más información sobre un fuego",
"esDecirTalDia": "Es decir el {{date}}",
"Actualizando...": "Actualizando..."
"Actualizando...": "Actualizando...",
"Alerta de fuego": "Alerta de fuego"
}

View file

@ -1 +1,223 @@
{}
{
"fireDetected": "lume detectado {{when}}",
"fireDetectedAt": "lume detectado o {{when}}",
"Sí": "Si",
"No": "Non",
"AppName": "Tod@s contra o Lume",
"AppNameFull": "Tod@s contra o Lume!",
"AppDescrip": "Colaboración masiva contra os incendios",
"AppDescripLong": "Empregamos diferentes fontes de datos para notificarche os lumes activos nas túas zonas de interese",
"OrgName": "Comúns",
"OrgNameFull": "Asociación Comúns",
"Privacidad": "Privacidade",
"Términos": "Termos",
"Términos de Servicio": "Termos do Servizo",
"Inicio": "Inicio",
"Licencia": "Licencia",
"Cerrar sesión": "Pechar sesión",
"Registrarse": "Rexistrarse",
"Regístrate": "Rexístrate",
"Iniciar sesión": "Iniciar sesión",
"o regístrate con un correo": "ou rexístrate cun correo",
"o con un correo": "ou cun correo",
"Nombre": "Nome",
"Apellidos": "Apelidos",
"Correo electrónico": "Correo electrónico",
"Contraseña": "Contrasinal",
"¿Ya tienes un cuenta?": "Xa tes unha conta?",
"Usa al menos seis caracteres.": "Emprega polo menos seis caracteres.",
"Iniciar sesión con Google": "Iniciar sesión con Google",
"¿Olvidaste tu contraseña?": "Esqueciches o teu contrasinal?",
"¿No tienes una cuenta?": "Non tes unha conta?",
"Introduce tu correo abajo para recibir un enlace para resetear tu contraseña.": "Introduce o teu correo abaixo para recibir un enlace para resetear o teu contrasinal.",
"Recupera tu contraseña": "Recupera o teu contrasinal",
"¿Recuerdas tu contraseña?": "Lembras o teu contrasinal?",
"Necesitamos un correo aquí.": "Necesitamos un correo aquí.",
"¿Es este correo correcto?": "É iste correo correcto?",
"Necesitamos una contraseña aquí.": "Necesitamos un contrasinal aquí.",
"Bienvenid@ de nuevo": "Benvid@ de novo",
"Guardar perfíl": "Gardar perfil",
"Contraseña actual": "Contrasinal actual",
"Nueva contraseña": "Novo contrasinal",
"Editar perfíl": "Editar perfil",
"Editar perfíl en": "Editar perfil en",
"¡Perfíl actualizado!": "Perfil actualizado!",
"¿Cuál es tu nombre?": "Cal é o teu nome?",
"¿Cuál es tu apellido?": "Cal é o teu apelido?",
"Necesito tu contraseña si la quieres cambiar.": "Necesito o teu contrasinal se o queres cambiar.",
"Necesito tu nueva contraseña si la quieres cambiar.": "Necesito o teu novo contrasinal se o queres cambiar.",
"¿Es correcto este correo?": "É correcto iste correo?",
"verifyEmail": "Ei! pódesnos verificar o teu correo electrónico ({{email}})?",
"Reenviar email de verificación": "Reenviar email de verificación",
"checkVerificationEmail": "Comproba no teu correo {{email}} o enlace de verificación!",
"checkResetEmail": "Comproba no teu correo {{email}} o enlace de reseteo!",
"¡Listo, gracias!": "Listo, grazas!",
"Por favor, inténtalo otra vez.": "Por favor, téntao de novo.",
"Verificando...": "Verificando...",
"Introduce una nueva contraseña, por favor.": "Introduce un novo contrasinal, por favor.",
"Repite tu nueva contraseña, por favor.": "Repite o teu novo contrasinal, por favor.",
"Mmmm, tus contraseñas no coinciden. Inténtalo otra vez": "Mmmm, os teus contrasinais non coinciden. Téntao de novo",
"Para resetear tu contraseña, introduce una nueva debajo. Iniciarás la sesión con la nueva contraseña.": "Para resetear o teu contrasinal, introduce un novo debaixo. Iniciarás a sesión co novo contrasinal.",
"Resetea tu contraseña": "Resetea o teu contrasinal",
"Repite la nueva contraseña": "Repite o novo contrasinal",
"Resetea la contraseña y entra": "Resetea o contrasinal e entra",
"Uso de Cookies": "Emprego de Cookies",
"Utilizamos cookies para asegurar un mejor uso de nuestra web. Si continúas navegando, consideramos que aceptas su uso": "Empregamos cookies para asegurar un mellor uso da nosa web. Se continúas a navegar, consideramos que aceptas o seu uso",
"Participa": "Participa",
"activeFires": "Lumes activos",
"Fuegos activos": "Lumes activos",
"noActiveFireInMapCount": "Non hai lumes activos nesta zona do mapa. <1><0>{{countTotal}}</0></1> lumes activos no mundo.",
"activeFireInMapCount": "En vermello, <1><0>{{count,number}}</0></1> lumes activos. <3><0>{{countTotal,number}}</0></3> lumes activos no mundo.",
"activeNeigFireInMapCount": "En laranxa, os lumes notificados polas/os nosas/os usuarias/os recentemente.",
"Centrar en tu ubicación": "Centrar na túa ubicación",
"Resaltar en verde el área vigilada por nuestros usuarios/as": "Resaltar en verde a área vixiada por as/os nosas/os usuarias/os",
"Resaltar los fuegos con un marcador": "Resaltar os lumes cun marcador",
"mapPrivacy": "<0>Para preservar a privacidade das/os nosas/os usuarias/os, os datos reflectidos están aleatoriamente alterados e son só orientativos.</0>",
"Nuevas notificaciones de {{app}}": "Novas notificacións de {{app}}",
"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.": "Imaxes capturadas polos satélites da NASA amosan o fume de grandes incendios que se estenden sobre o Océano Pacífico. A actividade do lume esta deliñada en vermello.",
"Todavía sin subscriptiones": "Aínda sen subscricións",
"Suscripción añadida": "Suscrición engadida",
"Suscripción actualizada": "Suscrición actualizada",
"Desuscrito": "Desuscrita",
"Mis zonas": "As miñas zonas",
"Nueva zona": "Nova zona",
"Suscribirme a fuegos en este radio": "Suscribirme a lumes neste radio",
"Suscribirme a este radio": "Suscribirme a iste radio",
"Créditos": "Créditos",
"Escribe aquí un lugar": "Escribe aquí un lugar",
"Indícanos la posición de la zona a vigilar (por ej. tu pueblo, una calle, etc):": "Indícanos a posición da zona a vixiar (por ex. a túa aldea, unha rúa, etc):",
"También puedes seleccionar la zona en el mapa arrastrando el puntero naranja.": "Tamén podes seleccionar a zona no mapa arrastrando o punteiro laranxa.",
"¿A que distancia a la redonda quieres recibir notificaciones de fuegos?": "A que distancia darredor queres recibir notificacións de lumes?",
"Pulsa para activar": "Preme para activar",
"Arrastrar para seleccionar otro punto": "Arrastrar para seleccionar outro punto",
"Mapa gris de OpenStreetMap": "Mapa gris de OpenStreetMap",
"Mapa color de OpenStreetMap": "Mapa cor de OpenStreetMap",
"Mapa de carreteras de Google": "Mapa de estradas de Google",
"Mapa de terreno de Google": "Mapa de terreo de Google",
"Mapa de satélite de Google": "Mapa de satélite de Google",
"Zona añadida": "Zona engadida",
"Añadir zona": "Engadir zona",
"Editar": "Editar",
"Suscripciones a alertas de fuegos en zonas de mi interés": "Suscricións a alertas de lumes en zonas do meu interese",
"En verde, áreas de las que recibirás alertas de fuegos": "En verde, áreas das que recibirás alertas de lumes",
"Terminar": "Rematar",
"Pulsa para borrar": "Preme para borrar",
"Pulsa aquí para borrar la zona": "Preme aquí para borrar a zona",
"Los fuegos activos se actualizan en tiempo cuasi real.": "Os lumes activos actualízanse en tempo cuasi real.",
"Somos muchos ojos": "Somos moitos ollos",
"Usa nuestro bot de Telegram para estar al tanto de los fuegos en tus área": "Emprega o noso bot do Telegram para estar ó tanto dos lumes na túa área",
"Otros dispositivos": "Outros dispositivos",
"support-us-home": "Estamos a desenvolver as nosas ferramentas para outros dispositivos. Podes <1>contribuír a facelo posible.</1>",
"Software Libre": "Software Libre",
"Suscríbete a alertas de fuegos": "Suscríbete a alertas de lumes",
"Gracias por Participar": "Grazas por Participar",
"También puedes seguirnos en la web": "Tamén nos podes seguir na web",
"Participar": "Participar",
"Dejarás de recibir notificaciones de fuegos en esa área ¿Estás seguro/a? ": "Deixarás de recibir notificacións de lumes nesa área. Estás segura/o? ",
"Sobre los datos y imágenes usados": "Sobre os datos e imaxes empregados",
"dev-with-us-home": "Todo o noso traballo é <1>sofware libre</1>. <3>Traductoræs</3> e <5>desenvolvedoræs sempre benvid@s</5>.",
"Política de Privacidad": "Política de Privacidade",
"No estás suscrito a fuegos en ninguna zona": "Non tes suscrición a lumes en ningunha zona",
"Iniciar sesión con Telegram": "Iniciar sesión con Telegram",
"Idioma": "Idioma",
"Comentarios": "Comentarios",
"Guardar": "Gardar",
"Responder": "Responder",
"Borrar": "Borrar",
"Añadir un comentario": "Engadir un comentario",
"Añadir una respuesta": "Engadir unha resposta",
"Añadir comentario": "Engadir comentario",
"Necesitas iniciar sesión para": "Precisas iniciar a sesión para",
"añadir comentarios": "engadir comentarios",
"puntuar comentarios": "puntuar comentarios",
"responder": "responder",
"Más comentarios": "Máis comentarios",
"Elige un lugar": "Escolle un lugar",
"Elige un radio de vigilancia": "Escolle un radio de vixiancia",
"Recibe alertas de fuegos en esa zona": "Recibe alertas de lumes nesa zona",
"Alerta cuando hay un fuego": "Alerta cando hai un lume",
"Anterior": "Anterior",
"Siguiente": "Seguinte",
"Siempre alerta a los fuegos en nuestro vecindario": "Alerta sempre dos lumes na nosa veciñanza",
"Información adicional sobre fuego detectado en {{where}} el {{when}}": "Información adicional sobre lume detectado en {{where}} o {{when}}",
"Información adicional sobre fuego detectado el {{when}}": "Información adicional sobre lume detectado o {{when}}",
"Coordenadas:": "Coordenadas:",
"Fuego detectado por satélites de la NASA <1></1>": "Lume detectado por satélites da NASA <1></1>",
"Puedes añadir un comentario si tienes información adicional sobre este fuego.": "Podes engadir un comentario se tes información adicional sobre este lume",
"Por ejemplo:": "Por exemplo:",
"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)": "se coñeces esta zona e como acceder ó lume (isto pode ser de moita axuda para apagalo se segue activo ou para investigalo nun futuro)",
"si conoces el motivo por el que comenzó el fuego": "se coñeces o motivo polo que empezou o lume",
"si quieres denunciar algún tipo de ilegalidad, incluso anónimamente": "se queres denunciar algún tipo de ilegalidade, incluso anonimamente",
"o cualquier otra información": "ou calquera outra información",
"Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.": "Faga zoom nunha zona do seu interese se quere que os lumes se actualicen en tempo real.",
"Notificaciones": "Notificacións",
"Recibe nuestras notificaciones de fuegos por correo o en tu navegador": "Recibe as nosas notificacións de lumes por correo ou no teu navegador",
"Fuego notificado por uno de nuestros usuarios/as <1></1>": "Lume notificado por un/ha das/os nosas/os usuarias/os",
"No recibirás notificaciones de fuegos en este equipo, solo por correo": "Non recibirás notificacións de lumes neste equipo, só por correo",
"not-found": "Upppps: Esta páxina non existe",
"general-error-title": "Upppps: Algo saiu mal",
"general-error-description": "Estamos a investigar o problema, volve tentalo nun cachiño",
"Más información sobre este fuego": "Máis información sobre este lume",
"Última actualización, {{when}}": "Última actualización, {{when}}",
"termsAccept": "Acepto as <1>condicións de servizo</1> deste sitio",
"Bienvenid@!": "Benvid@!",
"welcome": "Benvid@",
"Has iniciado sesión con {{service}} usando la dirección de correo {{email}}.": "Iniciaches a sesión con {{service}} usando a dirección de correo {{email}}.",
"Verifica tu dirección de correo": "Verifica a túa dirección de correo",
"Zonas vigiladas": "Zonas vixiladas",
"En verde, las zonas vigiladas por nuestros usuari@s actualmente": "En verde, as zonas vixiladas polas/os nosas/os usuarias/os actualmente",
"Actualizado <1></1>, último fuego detectado <3></3>.": "Actualizado <1></1>, último lume detectado <3></3>.",
"Tomamos nota, ¡gracias por colaborar!": "Tomamos nota, grazas por colaborar!",
"Indícanos de que tipo de fuego se trata y ayúdanos así a mejorar nuestras notificaciones:": "Indícanos de que tipo de lume se trata e axúdanos así a mellorar as nosas notificacións:",
"¿No es un fuego forestal?": "Non é un lume forestal?",
"Regístrate o inicia sesión para aportar información sobre este fuego": "Rexístrate ou inicia sesión para aportar información sobre este lume",
"Fuego no encontrado": "Lume non atopado",
"Es una industria": "É unha industria",
"Es una quema controlada": "É unha queima controlada",
"Parece una falsa alarma": "Parece unha falsa alarma",
"Fuegos activos en el mundo actualizados en tiempo real": "Lumes activos no mundo actualizados en tempo real",
"Información adicional sobre fuego": "Información adicional sobre o lume",
"CO2emisions": "Sabías que os lumes <1>producen tanto CO² como os coches</1> e <3>aproximadamente ⅕ de tódalas nosas emisións </3>?",
"Ayúdanos a combatir el cambio climático y a proteger el medioambiente": "Axúdanos a combatir o cambio climático e a protexer o medioambiente",
"Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017": "Lume en La Tuna, Los Ángeles, Estados Unidos, setembro do 2017",
"Polución en la ciudad de Almaty, Kazakhstan, enero de 2014": "Polución na cidade de Almaty, Kazakhstan, xaneiro do 2014",
"Fuente": "Fonte",
"NASA": "NASA",
"nuestros usuarios/as": "as nosas/os usuarias/os",
"Pulsa para más información": "Preme para máis información",
"Detectado": "Detectado",
"Zonas vigiladas por nuestros usuari@s actualmente": "Zonas vixiadas pol@s nos@s usuari@s actualmente",
"Iniciar sesión con Facebook": "Iniciar sesión con Facebook",
"Elige un tipo": "Escolle un tipo",
"Puedes participar en las traducciones": "Podes participar nas traducións",
"Aceptar": "Aceptar",
"Por favor, escribe aquí tu feedback...": "Por favor, escribe aquí o teu feedback...",
"Feedback": "Feedback",
"Enviar": "Enviar",
"Tu correo": "O teu correo",
"Desconectado del servidor, reconectando en %delay% segundos.": "Desconectando di servidor, reconectando en %delay% segundos.",
"Desconectado del servidor, reconectando...": "Desconectado do servidor, reconectando...",
"Reintentar ahora": "Reintentar agora",
"Feedback recibido, gracias...": "Feedback recibido, grazas...",
"Parece que este fuego no es un fuego forestal.": "Parece que este lume non é un lume forestal.",
"Sobre nosotr@s": "Sobre nós",
"Nosotr@s": "Nós",
"Sobre '{{appName}}'": "Sobre '{{appName}}'",
"¿Vecindarios vigilando y combatiendo fuegos? ¿de qué va todo esto?": "Veciñanzas vixiando e combatendo lumes? de vai todo isto?",
"noActiveNeigFireInMap": "Non hai lumes notificados recentemente polas/os nosas/os usuarias/os nesta zona.",
"Es una industria (fuente: registro oficial)": "É unha industria (fonte: rexistro oficial)",
"Es una industria (fuente: nuestros usuarios/as)": "É unha industria (fonte as/os nosas/os usuarias/os):",
"Pantalla completa": "Pantalla completa",
"Salir de pantalla completa": "Saír da pantalla completa",
"Escribe un lugar para centrar el mapa": "Escribe un lugar para centrar o mapa",
"geo-not-perms-error": "Upppps: parece que non temos permiso para coñecer a túa ubicación",
"geo-not-avail-error": "Upppps: parece que a túa ubicación non está dispoñible",
"geo-not-timeout-error": "Upppps: o teu dispositivo está a tardar demasiado en coñecer a túa ubicación",
"Estás subscrito a una zona muy grande": "Suscribícheste a unha zona grande de máis",
"Lugar no encontrado": "Lugar non atopado",
"on-already-subscribed": "Upppps: Xa te suscribiras a esta zona antes",
"Cancelar": "Cancelar",
"Nota: Las nubes pueden entorpecer la detección de fuegos activos.": "Nota: As nubes poden entorpecer a detección de lumes activos.",
"Hay más información sobre un fuego": "Hai máis información sobre un lume",
"esDecirTalDia": "É dicir o {{date}}"
}

View file

@ -31,6 +31,7 @@
"loginStyle": "popup"
}
},
"fcmApiToken": "",
"proxies_count": 0,
"ironPassword": "SomePasswordAlLeast32LongYouKnowHowToCount",
"internalApiToken": "someSimilarPasswordForUseOurAPIinternally",

View file

@ -0,0 +1,29 @@
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
/* eslint-disable import/no-absolute-path */
import { chai } from 'meteor/practicalmeteor:chai';
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
const centerid = { type: 'Point', coordinates: [-5.905956029891968, 43.55727622097011] };
const shape = { type: 'Polygon', coordinates: [[[-5.9084, 43.5558], [-5.906, 43.5558], [-5.906, 43.5566], [-5.9036, 43.5566], [-5.9036, 43.5583], [-5.9061, 43.5583], [-5.9061, 43.558], [-5.9084, 43.558], [-5.9084, 43.5558]]] };
const fireUnion = { centerid, shape, when: Date('2018-05-02T16:11:04.617Z') };
describe('activeFireUnion insert', () => {
it('should insert fire union', () => {
// Remove previous test register
const alreadyInserted = ActiveFiresUnion.findOne({ centerid });
if (alreadyInserted) {
ActiveFiresUnion.remove(alreadyInserted._id);
}
const insertedId = ActiveFiresUnion.insert(fireUnion);
ActiveFiresUnion.schema.validate(fireUnion);
const inserted = ActiveFiresUnion.findOne(insertedId);
delete inserted._id;
chai.expect(fireUnion).to.deep.equal(inserted);
ActiveFiresUnion.remove(insertedId);
chai.expect(ActiveFiresUnion.find({ _id: inserted }).count()).equal(0);
});
});

View file

@ -91,8 +91,9 @@ describe('basic api v1 returns', () => {
}));
const mobileToken = 'user-mobile-firebase-token';
let testUserId;
let returnedSubsId;
const objId = new Meteor.Collection.ObjectID();
it('should create mobile users', async (done) => {
// this are removed in the test database but we are testing in dev/ci database
@ -139,11 +140,8 @@ describe('basic api v1 returns', () => {
});
});
let subsId;
function addSubs(done) {
// this are removed in the test database but we are testing in dev/ci database
// Remove previous subs of this user
// Subscriptions.remove({ owner: testUserId });
// chai.expect(Subscriptions.find({ owner: testUserId }).count()).to.equal(0);
@ -151,16 +149,19 @@ describe('basic api v1 returns', () => {
data: {
token,
mobileToken,
id: objId._str,
lat: 3.106111,
lon: 53.5775,
distance: 100
}
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 200) console.log(result);
chai.expect(result.statusCode).equal(200);
const jsendResult = result.data;
chai.expect(jsendResult.status).equal('success');
subsId = jsendResult.data.subsId;
returnedSubsId = jsendResult.data.subsId;
chai.expect(returnedSubsId.toString()).equal(objId._str);
done();
});
}
@ -174,30 +175,30 @@ describe('basic api v1 returns', () => {
data: {
token: 'wrongOne',
mobileToken,
id: objId._str,
lat: 3.106111,
lon: 53.5775,
distance: 100
}
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 401) console.log(result);
chai.expect(result.statusCode).equal(401);
done();
});
});
it('should get all mobile user subscriptions', async (done) => {
HTTP.get(url('api/v1/mobile/subscriptions/all'), {
data: {
token,
mobileToken
}
HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), {
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 200) console.log(result);
chai.expect(result.statusCode).equal(200);
const jsendResult = result.data;
chai.expect(jsendResult.status).equal('success');
chai.expect(typeof jsendResult.data.subscriptions).to.equal('object');
chai.expect(typeof jsendResult.data.count).to.equal('number');
if (jsendResult.data.count !== 1) console.log(result);
chai.expect(jsendResult.data.count).to.equal(1);
// console.log(JSON.stringify(jsendResult.data.subscriptions));
done();
@ -208,14 +209,10 @@ describe('basic api v1 returns', () => {
it('should remove mobile user subscriptions', async (done) => {
// this are removed in the test database but we are testing in dev/ci database
HTTP.del(url('api/v1/mobile/subscriptions'), {
data: {
token,
mobileToken,
subsId
}
HTTP.del(url(`api/v1/mobile/subscriptions/${token}/${mobileToken}/${returnedSubsId}`), {
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 200) console.log(result);
chai.expect(result.statusCode).equal(200);
const jsendResult = result.data;
chai.expect(jsendResult.status).equal('success');
@ -223,47 +220,55 @@ describe('basic api v1 returns', () => {
});
});
it('should not remove mobile user subscriptions with wrong token', async (done) => {
HTTP.del(url('api/v1/mobile/subscriptions'), {
data: {
token: 'wrongOne',
mobileToken,
subsId
}
it('should get all mobile user subscriptions after deletion', async (done) => {
HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), {
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 200) console.log(result);
chai.expect(result.statusCode).equal(200);
const jsendResult = result.data;
chai.expect(jsendResult.status).equal('success');
chai.expect(typeof jsendResult.data.subscriptions).to.equal('object');
chai.expect(typeof jsendResult.data.count).to.equal('number');
if (jsendResult.data.count !== 0) console.log(result);
chai.expect(jsendResult.data.count).to.equal(0);
// console.log(JSON.stringify(jsendResult.data.subscriptions));
done();
});
});
it('should not remove mobile user subscriptions with wrong token', async (done) => {
HTTP.del(url(`api/v1/mobile/subscriptions/wrongOne/${mobileToken}/${returnedSubsId}`), {
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 401) console.log(result);
chai.expect(result.statusCode).equal(401);
done();
});
});
it('should not get mobile user subscriptions with wrong token', async (done) => {
HTTP.get(url('api/v1/mobile/subscriptions/all'), {
data: {
token: 'wrongOne',
mobileToken
}
HTTP.get(url(`api/v1/mobile/subscriptions/all/wrongOne/${mobileToken}`), {
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 401) console.log(result);
chai.expect(result.statusCode).equal(401);
done();
});
});
it('should get all mobile user subscriptions', async (done) => {
HTTP.get(url('api/v1/mobile/subscriptions/all'), {
data: {
token,
mobileToken
}
HTTP.get(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), {
}, (error, result) => {
chai.expect(error, null);
// console.log(result);
if (result.statusCode !== 200) console.log(result);
chai.expect(result.statusCode).equal(200);
const jsendResult = result.data;
chai.expect(jsendResult.status).equal('success');
chai.expect(typeof jsendResult.data.subscriptions).to.equal('object');
chai.expect(typeof jsendResult.data.count).to.equal('number');
if (jsendResult.data.count !== 0) console.log(result);
chai.expect(jsendResult.data.count).to.equal(0);
done();
});
@ -272,18 +277,15 @@ describe('basic api v1 returns', () => {
it('should del all mobile user subscriptions', async (done) => {
// Add subs
addSubs(() => {
HTTP.del(url('api/v1/mobile/subscriptions/all'), {
data: {
token,
mobileToken
}
HTTP.del(url(`api/v1/mobile/subscriptions/all/${token}/${mobileToken}`), {
}, (error, result) => {
chai.expect(error, null);
if (result.statusCode !== 200) console.log(result);
chai.expect(result.statusCode).equal(200);
const jsendResult = result.data;
chai.expect(jsendResult.status).equal('success');
chai.expect(typeof jsendResult.data.count).to.equal('number');
chai.expect(jsendResult.data.count).to.equal(1);
chai.expect(jsendResult.data.count).to.be.at.most(1);
done();
});
});