Merged in develop

This commit is contained in:
Keith Holliday 2017-06-27 22:23:13 -06:00
commit 7fd2522e93
897 changed files with 50434 additions and 41464 deletions

View file

@ -1,6 +1,6 @@
# Reporting Bugs
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitrpg/issues/2760)
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitica/issues/2760)
# Pull Request

View file

@ -4,7 +4,7 @@
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
### General Info

View file

@ -33,3 +33,4 @@ env:
- TEST="test:common" COVERAGE=true
- TEST="test:karma" COVERAGE=true
- TEST="client:unit" COVERAGE=true
- TEST="apidoc"

View file

@ -3,7 +3,7 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
/**
* database_reports/count_users_who_own_specified_gear.js
* https://github.com/HabitRPG/habitrpg/pull/3884
* https://github.com/HabitRPG/habitica/pull/3884
*/
var thingsOfInterest = {

36
gulp/gulp-bootstrap.js Normal file
View file

@ -0,0 +1,36 @@
import gulp from 'gulp';
import fs from 'fs';
// Copy Bootstrap 4 config variables from /website /node_modules so we can check
// them into Git
const BOOSTRAP_NEW_CONFIG_PATH = 'website/client/assets/scss/bootstrap_config.scss';
const BOOTSTRAP_ORIGINAL_CONFIG_PATH = 'node_modules/bootstrap/scss/_custom.scss';
// https://stackoverflow.com/a/14387791/969528
function copyFile(source, target, cb) {
let cbCalled = false;
function done(err) {
if (!cbCalled) {
cb(err);
cbCalled = true;
}
}
let rd = fs.createReadStream(source);
rd.on('error', done);
let wr = fs.createWriteStream(target);
wr.on('error', done);
wr.on('close', () => done());
rd.pipe(wr);
}
gulp.task('bootstrap', (done) => {
// use new config
copyFile(
BOOSTRAP_NEW_CONFIG_PATH,
BOOTSTRAP_ORIGINAL_CONFIG_PATH,
done,
);
});

View file

@ -49,7 +49,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
});
if (numberOfSheetsThatAreTooBig > 0) {
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitrpg/pull/6683#issuecomment-185462180
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
} else {
console.log('All images are within the correct dimensions');
}

View file

@ -13,6 +13,7 @@ if (process.env.NODE_ENV === 'production') {
require('./gulp/gulp-newstuff');
require('./gulp/gulp-build');
require('./gulp/gulp-babelify');
require('./gulp/gulp-bootstrap');
} else {
require('glob').sync('./gulp/gulp-*').forEach(require);
require('gulp').task('default', ['test']);

View file

@ -0,0 +1,110 @@
'use strict';
/****************************************
* Author: @Alys
*
* Reason: Collection quests are being changed
* to require fewer items collected:
* https://github.com/HabitRPG/habitrpg/pull/7987
* This will cause existing quests to end sooner
* than the party is expecting.
* This script inserts an explanatory `system`
* message into the chat for affected parties.
***************************************/
global.Promise = require('bluebird');
const uuid = require('uuid');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
const connectToDb = require('./utils/connect').connectToDb;
const closeDb = require('./utils/connect').closeDb;
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.wikia.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
const timer = new Timer();
// PROD: Enable prod db
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
const DB_URI = 'mongodb://localhost/habitrpg';
const COLLECTION_QUESTS = [
'vice2',
'egg',
'moonstone1',
'goldenknight1',
'dilatoryDistress1',
];
let Groups;
connectToDb(DB_URI).then((db) => {
Groups = db.collection('groups');
return Promise.resolve();
})
.then(findPartiesWithCollectionQuest)
// .then(displayGroups) // for testing only
.then(addMessageToGroups)
.then(() => {
timer.stop();
closeDb();
}).catch(reportError);
function reportError (err) {
logger.error('Uh oh, an error occurred');
closeDb();
timer.stop();
throw err;
}
function findPartiesWithCollectionQuest () {
logger.info('Looking up groups on collection quests...');
return Groups.find({'quest.key': {$in: COLLECTION_QUESTS}}, ['name','quest']).toArray().then((groups) => {
logger.success('Found', groups.length, 'parties on collection quests');
return Promise.resolve(groups);
})
}
function displayGroups (groups) { // for testing only
logger.info('Displaying parties...');
console.log(groups);
return Promise.resolve(groups);
}
function updateGroupById (group) {
var newMessage = {
'id' : uuid.v4(),
'text' : message,
'timestamp': Date.now(),
'likes': {},
'flags': {},
'flagCount': 0,
'uuid': 'system'
};
return Groups.findOneAndUpdate({_id: group._id}, {$push:{"chat" :{$each: [newMessage], $position:0}}}, {returnOriginal: false});
// Does not set the newMessage flag for all party members because I don't think it's essential and
// I don't want to run the extra code (extra database load, extra opportunity for bugs).
}
function addMessageToGroups (groups) {
let queue = new TaskQueue(Promise, 300);
logger.info('About to update', groups.length, 'parties...');
return Promise.map(groups, queue.wrap(updateGroupById)).then((result) => {
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
logger.success(updates.length, 'parties have been notified');
if (failures.length > 0) {
logger.error(failures.length, 'parties could not be notified');
}
return Promise.resolve();
});
}

View file

@ -0,0 +1,114 @@
var migrationName = '20170616_achievements';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Updates to achievements for June 16, 2017 biweekly merge
* 1. Multiply various collection quest achievements based on difficulty reduction
* 2. Award Joined Challenge achievement to those who should have it already
*/
import monk from 'monk';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
$or: [
{'achievements.quests.dilatoryDistress1': {$gt:0}},
{'achievements.quests.egg': {$gt:0}},
{'achievements.quests.goldenknight1': {$gt:0}},
{'achievements.quests.moonstone1': {$gt:0}},
{'achievements.quests.vice2': {$gt:0}},
{'achievements.challenges': {$exists: true, $ne: []}},
{'challenges': {$exists: true, $ne: []}},
],
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
'achievements',
'challenges',
],
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {'migration': migrationName};
if (user.challenges.length > 0 || user.achievements.challenges.length > 0) {
set['achievements.joinedChallenge'] = true;
}
if (user.achievements.quests.dilatoryDistress1) {
set['achievements.quests.dilatoryDistress1'] = Math.ceil(user.achievements.quests.dilatoryDistress1 * 1.25);
}
if (user.achievements.quests.egg) {
set['achievements.quests.egg'] = Math.ceil(user.achievements.quests.egg * 2.5);
}
if (user.achievements.quests.goldenknight1) {
set['achievements.quests.goldenknight1'] = user.achievements.quests.goldenknight1 * 5;
}
if (user.achievements.quests.moonstone1) {
set['achievements.quests.moonstone1'] = user.achievements.quests.moonstone1 * 5;
}
if (user.achievements.quests.vice2) {
set['achievements.quests.vice2'] = Math.ceil(user.achievements.quests.vice2 * 1.5);
}
dbUsers.update({_id: user._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;

View file

@ -1,47 +1,47 @@
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
async function syncChallengeToMembers (challenges) {
let challengSyncPromises = challenges.map(async function (challenge) {
let users = await User.find({
// _id: '',
challenges: challenge._id,
}).exec();
let promises = [];
users.forEach(function (user) {
promises.push(challenge.syncToUser(user));
promises.push(challenge.save());
promises.push(user.save());
});
return Bluebird.all(promises);
});
return await Bluebird.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
let query = {
// _id: '',
};
if (lastChallengeDate) {
query.createdOn = { $lte: lastChallengeDate };
}
let challengesFound = await Challenges.find(query)
.limit(10)
.sort('-createdAt')
.exec();
let syncedChallenges = await syncChallengeToMembers(challengesFound)
.catch(reason => console.error(reason));
let lastChallenge = challengesFound[challengesFound.length - 1];
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
return syncedChallenges;
};
module.exports = syncChallenges;
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
async function syncChallengeToMembers (challenges) {
let challengSyncPromises = challenges.map(async function (challenge) {
let users = await User.find({
// _id: '',
challenges: challenge._id,
}).exec();
let promises = [];
users.forEach(function (user) {
promises.push(challenge.syncToUser(user));
promises.push(challenge.save());
promises.push(user.save());
});
return Bluebird.all(promises);
});
return await Bluebird.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
let query = {
// _id: '',
};
if (lastChallengeDate) {
query.createdOn = { $lte: lastChallengeDate };
}
let challengesFound = await Challenges.find(query)
.limit(10)
.sort('-createdAt')
.exec();
let syncedChallenges = await syncChallengeToMembers(challengesFound)
.catch(reason => console.error(reason));
let lastChallenge = challengesFound[challengesFound.length - 1];
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
return syncedChallenges;
};
module.exports = syncChallenges;

View file

@ -21,4 +21,4 @@ var processUsers = require('./groups/update-groups-with-group-plans');
processUsers()
.catch(function (err) {
console.log(err)
})
})

View file

@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['body_mystery_201705','head_mystery_201705']
$each:['body_mystery_201706','back_mystery_201706']
}
}
};

View file

@ -0,0 +1,109 @@
var migrationName = 'UserFromProdToTest';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migraition will copy user data from prod to test
*/
var monk = require('monk');
var testConnectionSting = ''; // FOR TEST DATABASE
var usersTest = monk(testConnectionSting).get('users', { castIds: false });
var groupsTest = monk(testConnectionSting).get('groups', { castIds: false });
var challengesTest = monk(testConnectionSting).get('challenges', { castIds: false });
var tasksTest = monk(testConnectionSting).get('tasks', { castIds: false });
var monk2 = require('monk');
var liveConnectString = ''; // FOR TEST DATABASE
var userLive = monk2(liveConnectString).get('users', { castIds: false });
var groupsLive = monk2(liveConnectString).get('groups', { castIds: false });
var challengesLive = monk2(liveConnectString).get('challenges', { castIds: false });
var tasksLive = monk2(liveConnectString).get('tasks', { castIds: false });
import uniq from 'lodash/uniq';
import Bluebird from 'bluebird';
// Variabls for updating
let userIds = [
'206039c6-24e4-4b9f-8a31-61cbb9aa3f66',
];
let groupIds = [];
let challengeIds = [];
let tasksIds = [];
async function processUsers () {
let userPromises = [];
//{_id: {$in: userIds}}
return userLive.find({guilds: 'b0764d64-8276-45a1-afa5-5ca9a5c64ca0'})
.each((user, {close, pause, resume}) => {
if (user.guilds.length > 0) groupIds = groupIds.concat(user.guilds);
if (user.party._id) groupIds.push(user.party._id);
if (user.challenges.length > 0) challengeIds = challengeIds.concat(user.challenges);
if (user.tasksOrder.rewards.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.rewards);
if (user.tasksOrder.todos.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.todos);
if (user.tasksOrder.dailys.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.dailys);
if (user.tasksOrder.habits.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.habits);
let userPromise = usersTest.update({'_id': user._id}, user, {upsert:true});
userPromises.push(userPromise);
}).then(() => {
return Bluebird.all(userPromises);
})
.then(() => {
console.log("Done User");
});
}
function processGroups () {
let promises = [];
let groupsToQuery = uniq(groupIds);
return groupsLive.find({_id: {$in: groupsToQuery}})
.each((group, {close, pause, resume}) => {
let promise = groupsTest.update({_id: group._id}, group, {upsert:true});
promises.push(promise);
}).then(() => {
return Bluebird.all(promises);
})
.then(() => {
console.log("Done Group");
});
}
function processChallenges () {
let promises = [];
let challengesToQuery = uniq(challengeIds);
return challengesLive.find({_id: {$in: challengesToQuery}})
.each((challenge, {close, pause, resume}) => {
let promise = challengesTest.update({_id: challenge._id}, challenge, {upsert:true});
promises.push(promise);
}).then(() => {
return Bluebird.all(promises);
})
.then(() => {
console.log("Done Challenge");
});
}
function processTasks () {
let promises = [];
let tasksToQuery = uniq(tasksIds);
return tasksLive.find({_id: {$in: tasksToQuery}})
.each((task, {close, pause, resume}) => {
let promise = tasksTest.update({_id: task._id}, task, {upsert:true});
promises.push(promise);
}).then(() => {
return Bluebird.all(promises);
})
.then(() => {
console.log("Done Tasks");
});
}
module.exports = async function prodToTest () {
await processUsers();
await processGroups();
await processChallenges();
await processTasks();
};

80
npm-shrinkwrap.json generated
View file

@ -1,6 +1,6 @@
{
"name": "habitica",
"version": "3.94.0",
"version": "3.98.1",
"dependencies": {
"@gulp-sourcemaps/map-sources": {
"version": "1.0.0",
@ -541,6 +541,11 @@
"from": "babel-core@>=6.0.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz"
},
"babel-eslint": {
"version": "7.2.3",
"from": "babel-eslint@latest",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz"
},
"babel-generator": {
"version": "6.24.1",
"from": "babel-generator@>=6.24.1 <7.0.0",
@ -630,6 +635,11 @@
"from": "babel-plugin-syntax-async-functions@>=6.13.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz"
},
"babel-plugin-syntax-dynamic-import": {
"version": "6.18.0",
"from": "babel-plugin-syntax-dynamic-import@latest",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz"
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"from": "babel-plugin-syntax-object-rest-spread@>=6.8.0 <7.0.0",
@ -5424,6 +5434,11 @@
"from": "he@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz"
},
"hellojs": {
"version": "1.15.1",
"from": "hellojs@>=1.15.1 <2.0.0",
"resolved": "http://registry.npmjs.org/hellojs/-/hellojs-1.15.1.tgz"
},
"hmac-drbg": {
"version": "1.0.1",
"from": "hmac-drbg@>=1.0.0 <2.0.0",
@ -7013,7 +7028,7 @@
"lazy-debug-legacy": {
"version": "0.0.1",
"from": "lazy-debug-legacy@>=0.0.0 <0.1.0",
"resolved": "http://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz"
"resolved": "https://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz"
},
"lazy-req": {
"version": "1.1.0",
@ -10808,6 +10823,11 @@
"resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz",
"dev": true
},
"simple-html-tokenizer": {
"version": "0.1.1",
"from": "simple-html-tokenizer@>=0.1.1 <0.2.0",
"resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz"
},
"simple-is": {
"version": "0.2.0",
"from": "simple-is@>=0.2.0 <0.3.0",
@ -11507,6 +11527,28 @@
"from": "supports-color@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
},
"svg-inline-loader": {
"version": "0.7.1",
"from": "svg-inline-loader@latest",
"resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.7.1.tgz"
},
"svg-url-loader": {
"version": "2.0.2",
"from": "svg-url-loader@latest",
"resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-2.0.2.tgz",
"dependencies": {
"file-loader": {
"version": "0.10.0",
"from": "file-loader@0.10.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.10.0.tgz"
},
"loader-utils": {
"version": "0.2.16",
"from": "loader-utils@0.2.16",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.16.tgz"
}
}
},
"svgo": {
"version": "0.7.2",
"from": "svgo@>=0.7.0 <0.8.0",
@ -11534,6 +11576,18 @@
}
}
},
"svgo-loader": {
"version": "1.2.1",
"from": "svgo-loader@latest",
"resolved": "https://registry.npmjs.org/svgo-loader/-/svgo-loader-1.2.1.tgz",
"dependencies": {
"loader-utils": {
"version": "1.1.0",
"from": "loader-utils@>=1.0.3 <2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz"
}
}
},
"syntax-error": {
"version": "1.3.0",
"from": "syntax-error@>=1.1.1 <2.0.0",
@ -12517,16 +12571,16 @@
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.3.1.tgz",
"dependencies": {
"async": {
"version": "2.4.0",
"from": "async@>=2.1.2 <3.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz"
"version": "2.4.1",
"from": "async@^2.1.2",
"resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz"
}
}
},
"webpack": {
"version": "2.5.1",
"from": "webpack@>=2.2.1 <3.0.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-2.5.1.tgz",
"version": "2.6.1",
"from": "webpack@2.6.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-2.6.1.tgz",
"dependencies": {
"acorn": {
"version": "5.0.3",
@ -12534,9 +12588,9 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz"
},
"async": {
"version": "2.4.0",
"version": "2.4.1",
"from": "async@>=2.1.2 <3.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz"
"resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz"
},
"source-list-map": {
"version": "1.1.2",
@ -12549,9 +12603,9 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz"
},
"uglify-js": {
"version": "2.8.26",
"from": "uglify-js@>=2.8.5 <3.0.0",
"resolved": "http://registry.npmjs.org/uglify-js/-/uglify-js-2.8.26.tgz",
"version": "2.8.28",
"from": "uglify-js@>=2.8.27 <3.0.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.28.tgz",
"dependencies": {
"yargs": {
"version": "3.10.0",

View file

@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.94.0",
"version": "3.98.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@ -15,8 +15,10 @@
"aws-sdk": "^2.0.25",
"axios": "^0.16.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-async-to-module-method": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
@ -67,6 +69,7 @@
"gulp-uglify": "^1.4.2",
"gulp.spritesmith": "^4.1.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^2.8.1",
"image-size": "~0.3.2",
"in-app-purchase": "^1.1.6",
@ -109,6 +112,9 @@
"shelljs": "^0.7.6",
"stripe": "^4.2.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.7.1",
"svg-url-loader": "^2.0.2",
"svgo-loader": "^1.2.1",
"universal-analytics": "~0.3.2",
"url-loader": "^0.5.7",
"useragent": "^2.1.9",
@ -135,7 +141,7 @@
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
"test": "npm run lint && gulp test && npm run client:unit",
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",
"test:api-v3:unit": "gulp test:api-v3:unit",
@ -152,14 +158,15 @@
"test:nodemon": "gulp test:nodemon",
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
"sprites": "gulp sprites:compile",
"client:dev": "node webpack/dev-server.js",
"client:build": "node webpack/build.js",
"client:dev": "gulp bootstrap && node webpack/dev-server.js",
"client:build": "gulp bootstrap && node webpack/build.js",
"client:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
"client:unit:watch": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js",
"client:e2e": "node test/client/e2e/runner.js",
"client:test": "npm run client:unit && npm run client:e2e",
"start": "gulp run:dev",
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
"postinstall": "bower --config.interactive=false install -f && gulp build && npm run client:build",
"apidoc": "gulp apidoc"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
@ -207,6 +214,7 @@
"nightwatch": "^0.9.12",
"phantomjs-prebuilt": "^2.1.12",
"protractor": "^3.1.1",
"raw-loader": "^0.5.1",
"require-again": "^2.0.0",
"rewire": "^2.3.3",
"selenium-server": "^3.0.1",

View file

@ -304,5 +304,15 @@ describe('POST /challenges', () => {
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
});
it('awards achievement if this is creator\'s first challenge', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
});
});
});

View file

@ -123,5 +123,12 @@ describe('POST /challenges/:challengeId/join', () => {
await expect(authorizedUser.get('/tags')).to.eventually.have.length(userTagsLength + 1);
});
it('awards achievement if this is user\'s first challenge', async () => {
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await authorizedUser.sync();
expect(authorizedUser.achievements.joinedChallenge).to.be.true;
});
});
});

View file

@ -100,8 +100,8 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
expect(winningUser.notifications.length).to.equal(1);
expect(winningUser.notifications[0].type).to.equal('WON_CHALLENGE');
expect(winningUser.notifications.length).to.equal(2); // 2 because winningUser just joined the challenge, which now awards an achievement
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
});
it('gives winner gems as reward', async () => {

View file

@ -84,6 +84,10 @@ describe('POST /chat/:chatId/flag', () => {
type: 'party',
privacy: 'private',
});
await user.post(`/groups/${privateGroup._id}/invite`, {
uuids: [anotherUser._id],
});
await anotherUser.post(`/groups/${privateGroup._id}/join`);
let { message } = await user.post(`/groups/${privateGroup._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await admin.post(`/groups/${privateGroup._id}/chat/${message.id}/flag`);
@ -91,7 +95,7 @@ describe('POST /chat/:chatId/flag', () => {
expect(flagResult.flags[admin._id]).to.equal(true);
expect(flagResult.flagCount).to.equal(5);
let groupWithFlags = await user.get(`/groups/${privateGroup._id}`);
let groupWithFlags = await anotherUser.get(`/groups/${privateGroup._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck).to.not.exist;
@ -125,4 +129,20 @@ describe('POST /chat/:chatId/flag', () => {
message: t('messageGroupChatFlagAlreadyReported'),
});
});
it('shows a hidden message to the original poster', async () => {
let { message } = await user.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
await admin.post(`/groups/${group._id}/chat/${message.id}/flag`);
let groupWithFlags = await user.get(`/groups/${group._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck).to.exist;
let auGroupWithFlags = await anotherUser.get(`/groups/${group._id}`);
let auMessageToCheck = find(auGroupWithFlags.chat, {id: message.id});
expect(auMessageToCheck).to.not.exist;
});
});

View file

@ -70,9 +70,9 @@ describe('POST /chat', () => {
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Your chat privileges have been revoked.',
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});

View file

@ -11,6 +11,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let guild;
let member;
let member2;
let adminUser;
beforeEach(async () => {
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
@ -28,6 +29,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
invitedUser = invitees[0];
member = members[0];
member2 = members[1];
adminUser = await generateUser({ 'contributor.admin': true });
});
context('All Groups', () => {
@ -42,7 +44,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
});
});
it('returns an error when user is a non-leader member of a group', async () => {
it('returns an error when user is a non-leader member of a group and not an admin', async () => {
expect(member2.post(`/groups/${guild._id}/removeMember/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
@ -87,7 +89,30 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let invitedUserWithoutInvite = await invitedUser.get('/user');
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
});
it('allows an admin to remove other members', async () => {
await adminUser.post(`/groups/${guild._id}/removeMember/${member._id}`);
let memberRemoved = await member.get('/user');
expect(memberRemoved.guilds.indexOf(guild._id)).eql(-1);
});
it('allows an admin to remove other invites', async () => {
await adminUser.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
let invitedUserWithoutInvite = await invitedUser.get('/user');
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
});
it('does not allow an admin to remove a leader', async () => {
expect(adminUser.post(`/groups/${guild._id}/removeMember/${leader._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
text: t('cannotRemoveCurrentLeader'),
});
});
it('sends email to user with rescinded invite', async () => {

View file

@ -1,10 +1,11 @@
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('PUT /group', () => {
let leader, nonLeader, groupToUpdate;
let leader, nonLeader, groupToUpdate, adminUser;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let groupUpdatedName = 'Test Public Guild Updated';
@ -18,13 +19,13 @@ describe('PUT /group', () => {
},
members: 1,
});
adminUser = await generateUser({ 'contributor.admin': true });
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
});
it('returns an error when a non group leader tries to update', async () => {
it('returns an error when a user that is not an admin or group leader tries to update', async () => {
await expect(nonLeader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
})).to.eventually.be.rejected.and.eql({
@ -44,6 +45,15 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows an admin to update a guild', async () => {
let updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
});
expect(updatedGroup.leader._id).to.eql(leader._id);
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows a leader to change leaders', async () => {
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,

View file

@ -0,0 +1,64 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /members/:toUserId/objections/:interaction', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(
user.get('/members/invalidUUID/objections/send-private-message')
).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing members', async () => {
let dummyId = generateUUID();
await expect(
user.get(`/members/${dummyId}/objections/send-private-message`)
).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: dummyId}),
});
});
it('handles non-existing interactions', async () => {
let receiver = await generateUser();
await expect(
user.get(`/members/${receiver._id}/objections/hug-a-whole-forest-of-trees`)
).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an empty array if there are no objections', async () => {
let receiver = await generateUser();
await expect(
user.get(`/members/${receiver._id}/objections/send-private-message`)
).to.eventually.be.fulfilled.and.eql([]);
});
it('returns an array of objections if any exist', async () => {
let receiver = await generateUser({'inbox.blocks': [user._id]});
await expect(
user.get(`/members/${receiver._id}/objections/send-private-message`)
).to.eventually.be.fulfilled.and.eql([
t('notAuthorizedToSendMessageToThisUser'),
]);
});
});

View file

@ -82,6 +82,20 @@ describe('POST /members/send-private-message', () => {
});
});
it('returns an error when chat privileges are revoked', async () => {
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
let receiver = await generateUser();
await expect(userWithChatRevoked.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('sends a private message to a user', async () => {
let receiver = await generateUser();

View file

@ -43,7 +43,7 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when to user is not found', async () => {
it('returns error when recipient is not found', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
@ -55,7 +55,7 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when to user attempts to send gems to themselves', async () => {
it('returns error when user attempts to send gems to themselves', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
@ -67,6 +67,64 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when recipient has blocked the sender', async () => {
let receiverWhoBlocksUser = await generateUser({'inbox.blocks': [userToSendMessage._id]});
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiverWhoBlocksUser._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('returns error when sender has blocked recipient', async () => {
let sender = await generateUser({'inbox.blocks': [receiver._id]});
await expect(sender.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('returns an error when chat privileges are revoked', async () => {
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('works when only the recipient\'s chat privileges are revoked', async () => {
let receiverWithChatRevoked = await generateUser({'flags.chatRevoked': true});
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiverWithChatRevoked._id,
})).to.eventually.be.fulfilled;
let updatedReceiver = await receiverWithChatRevoked.get('/user');
let updatedSender = await userToSendMessage.get('/user');
expect(updatedReceiver.balance).to.equal(gemAmount / 4);
expect(updatedSender.balance).to.equal(0);
});
it('returns error when there is no gemAmount', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
@ -144,7 +202,7 @@ describe('POST /members/transfer-gems', () => {
expect(updatedSender.balance).to.equal(0);
});
it('does not requrie a message', async () => {
it('does not require a message', async () => {
await userToSendMessage.post('/members/transfer-gems', {
gemAmount,
toUserId: receiver._id,

View file

@ -0,0 +1,25 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /shops/backgrounds', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns a valid shop object', async () => {
let shop = await user.get('/shops/backgrounds');
expect(shop.identifier).to.equal('backgroundShop');
expect(shop.text).to.eql(t('backgroundShop'));
expect(shop.notes).to.eql(t('backgroundShopText'));
expect(shop.imageName).to.equal('background_shop');
expect(shop.sets).to.be.an('array');
let sets = shop.sets.map(set => set.identifier);
expect(sets).to.include('incentiveBackgrounds');
expect(sets).to.include('backgrounds062014');
});
});

View file

@ -616,6 +616,18 @@ describe('POST /tasks/user', () => {
expect((new Date(task.startDate)).getDay()).to.eql(today);
});
it('returns an error if the start date is empty', async () => {
await expect(user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
startDate: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'daily validation failed',
});
});
it('can create checklists', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',

View file

@ -221,6 +221,27 @@ describe('POST /user/class/cast/:spellId', () => {
expect(syncedGroupTask.value).to.equal(0);
});
it('increases both user\'s achievement values', async () => {
let party = await createAndPopulateGroup({
members: 1,
});
let leader = party.groupLeader;
let recipient = party.members[0];
await leader.update({'stats.gp': 10});
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
await leader.sync();
await recipient.sync();
expect(leader.achievements.birthday).to.equal(1);
expect(recipient.achievements.birthday).to.equal(1);
});
it('only increases user\'s achievement one if target == caster', async () => {
await user.update({'stats.gp': 10});
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
await user.sync();
expect(user.achievements.birthday).to.equal(1);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'task\'');

View file

@ -525,6 +525,7 @@ describe('Amazon Payments', () => {
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
@ -555,6 +556,7 @@ describe('Amazon Payments', () => {
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
@ -593,6 +595,7 @@ describe('Amazon Payments', () => {
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
@ -623,6 +626,7 @@ describe('Amazon Payments', () => {
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});

View file

@ -79,6 +79,13 @@ describe('cron', () => {
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('resets plan.gemsBought on a new month if user does not have purchased.plan.dateUpdated', () => {
user.purchased.plan.gemsBought = 10;
user.purchased.plan.dateUpdated = undefined;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();

View file

@ -16,6 +16,7 @@ describe('payments/index', () => {
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
@ -504,6 +505,18 @@ describe('payments/index', () => {
expect(daysTillTermination).to.be.within(13, 15);
});
it('terminates at next billing date even if dateUpdated is prior to now', async () => {
data.nextBill = moment().add({ days: 15 });
data.user.purchased.plan.dateUpdated = moment().subtract({ days: 10 });
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
user.purchased.plan.extraMonths = 5;
@ -653,5 +666,32 @@ describe('payments/index', () => {
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('saves previously unused Mystery Items and Hourglasses for an expired subscription', async () => {
let planExpirationDate = new Date();
planExpirationDate.setDate(planExpirationDate.getDate() - 2);
let mysteryItem = 'item';
let mysteryItems = [mysteryItem];
let consecutive = {
trinkets: 3,
};
// set expired plan with unused items
plan.mysteryItems = mysteryItems;
plan.consecutive = consecutive;
plan.dateCreated = planExpirationDate;
plan.dateTerminated = planExpirationDate;
plan.customerId = null;
user.purchased.plan = plan;
await user.save();
await api.addSubToGroupUser(user, group);
let updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
});
});
});

View file

@ -138,7 +138,7 @@ describe('Canceling a subscription for group', () => {
]);
});
it('prevents non group leader from manging subscription', async () => {
it('prevents non group leader from managing subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;

View file

@ -1,5 +1,6 @@
import moment from 'moment';
import stripeModule from 'stripe';
import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
@ -12,17 +13,24 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
describe('Purchasing a subscription for group', () => {
describe('Purchasing a group plan for group', () => {
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS = 'iOS_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL = 'normal_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE = 'no_subscription';
let plan, group, user, data;
let stripe = stripeModule('test');
let groupLeaderName = 'sender';
let groupName = 'test group';
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.profile.name = groupLeaderName;
await user.save();
group = generateGroup({
name: 'test group',
name: groupName,
type: 'guild',
privacy: 'public',
leader: user._id,
@ -81,7 +89,7 @@ describe('Purchasing a subscription for group', () => {
sender.sendTxn.restore();
});
it('creates a subscription', async () => {
it('creates a group plan', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
@ -157,7 +165,7 @@ describe('Purchasing a subscription for group', () => {
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
});
it('sends an email to members of group', async () => {
it('sends an email to member of group who was not a subscriber', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
@ -169,11 +177,181 @@ describe('Purchasing a subscription for group', () => {
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE},
]);
// confirm that the other email sent is appropriate:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
});
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
});
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
amzLib.getBillingAgreementDetails.restore();
});
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: { // eslint-disable-line camelcase
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
cycles_completed: 1, // eslint-disable-line camelcase
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
});
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledFourTimes;
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[1][2]).to.eql([
{name: 'LEADER', content: groupLeaderName},
{name: 'GROUP_NAME', content: groupName},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE},
]);
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
});
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledFourTimes;
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[1][2]).to.eql([
{name: 'LEADER', content: groupLeaderName},
{name: 'GROUP_NAME', content: groupName},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS},
]);
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
});
it('adds months to members with existing gift subscription', async () => {
@ -333,7 +511,7 @@ describe('Purchasing a subscription for group', () => {
});
it('adds months to members with existing recurring subscription (Android)');
it('adds months to members with existing recurring subscription (iOs)');
it('adds months to members with existing recurring subscription (iOS)');
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
let recipient = new User();
@ -603,7 +781,7 @@ describe('Purchasing a subscription for group', () => {
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with a Google subscription', async () => {
it('does not modify a user with an Android subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;

View file

@ -447,6 +447,7 @@ describe('Paypal Payments', () => {
groupId,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
@ -464,6 +465,7 @@ describe('Paypal Payments', () => {
groupId: group._id,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
});

View file

@ -683,6 +683,7 @@ describe('Stripe Payments', () => {
groupId: undefined,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
@ -702,6 +703,7 @@ describe('Stripe Payments', () => {
groupId,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
});

View file

@ -228,7 +228,7 @@ describe('Group Model', () => {
});
it('applies damage only to participating members of party even under buggy conditions', async () => {
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitrpg/issues/7653
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitica/issues/7653
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,

View file

@ -112,6 +112,41 @@ describe('Groups Controller', function() {
});
});
describe('isAbleToEditGroup', () => {
var guild;
beforeEach(() => {
user.contributor = {};
guild = specHelper.newGroup({
_id: 'unique-guild-id',
type: 'guild',
members: ['not-user-id'],
$save: sandbox.spy(),
});
});
it('returns true if user is an admin', () => {
guild.leader = 'not-user-id';
user.contributor.admin = true;
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
});
it('returns true if user is group leader', () => {
guild.leader = {_id: user._id}
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
});
it('returns false is user is not a leader or admin', () => {
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
});
it('returns false is user is an admin but group is a party', () => {
guild.type = 'party';
user.contributor.admin = true;
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
});
});
describe('editGroup', () => {
var guild;

View file

@ -1,63 +1,63 @@
describe('Group Tasks Meta Actions Controller', () => {
let rootScope, scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
rootScope = $rootScope;
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
scope = $rootScope.$new();
scope.task = {
group: {
assignedUsers: [],
approval: {
required: false,
}
},
};
scope.task._edit = angular.copy(scope.task);
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
});
});
describe('toggleTaskRequiresApproval', function () {
it('toggles task approval required field from false to true', function () {
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.true;
});
it('toggles task approval required field from true to false', function () {
scope.task._edit.group.approval.required = true;
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.false;
});
});
describe('assign events', function () {
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
rootScope.$broadcast('addedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
});
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
scope.task.group.assignedUsers.push(testId);
scope.task._edit.group.assignedUsers.push(testId);
rootScope.$broadcast('removedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.not.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
});
});
});
describe('Group Tasks Meta Actions Controller', () => {
let rootScope, scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
rootScope = $rootScope;
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
scope = $rootScope.$new();
scope.task = {
group: {
assignedUsers: [],
approval: {
required: false,
}
},
};
scope.task._edit = angular.copy(scope.task);
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
});
});
describe('toggleTaskRequiresApproval', function () {
it('toggles task approval required field from false to true', function () {
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.true;
});
it('toggles task approval required field from true to false', function () {
scope.task._edit.group.approval.required = true;
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.false;
});
});
describe('assign events', function () {
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
rootScope.$broadcast('addedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
});
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
scope.task.group.assignedUsers.push(testId);
scope.task._edit.group.assignedUsers.push(testId);
rootScope.$broadcast('removedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.not.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
});
});
});

View file

@ -0,0 +1,20 @@
import roundBigNumberFilter from 'client/filters/roundBigNumber';
describe('round big number filter', () => {
it('can round a decimal number', () => {
expect(roundBigNumberFilter(4.567)).to.equal(4.57);
expect(roundBigNumberFilter(4.562)).to.equal(4.56);
});
it('can round thousands', () => {
expect(roundBigNumberFilter(70065)).to.equal('70.1k');
});
it('can round milions', () => {
expect(roundBigNumberFilter(10000987)).to.equal('10.0m');
});
it('can round bilions', () => {
expect(roundBigNumberFilter(1000000000)).to.equal('1.0b');
});
});

View file

@ -121,6 +121,17 @@ describe('achievements', () => {
});
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell'];
cardTypes.forEach((card) => {
let cardAchiev = basicAchievs[`${card}Cards`];
expect(cardAchiev).to.exist;
expect(cardAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
it('rebirth achievement exists with no count', () => {
let rebirth = basicAchievs.rebirth;
@ -174,7 +185,7 @@ describe('achievements', () => {
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
let cardTypes = ['nye', 'valentine'];
cardTypes.forEach((card) => {
let cardAchiev = seasonalAchievs[`${card}Cards`];

View file

@ -5,6 +5,7 @@ import 'moment-recur';
describe('shouldDo', () => {
let day, dailyTask;
let options = {};
let nextDue = [];
beforeEach(() => {
day = new Date();
@ -223,6 +224,25 @@ describe('shouldDo', () => {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
it('should compute daily nextDue values', () => {
options.timezoneOffset = 0;
options.nextDue = true;
day = moment('2017-05-01').toDate();
dailyTask.frequency = 'daily';
dailyTask.everyX = 2;
dailyTask.startDate = day;
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-05-03').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-05-05').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2017-05-07').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2017-05-09').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2017-05-11').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2017-05-13').toDate());
});
context('On multiples of x', () => {
it('returns true when Custom Day Start is midnight', () => {
dailyTask.startDate = moment().subtract(7, 'days').toDate();
@ -367,6 +387,73 @@ describe('shouldDo', () => {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
it('should compute weekly nextDue values', () => {
options.timezoneOffset = 0;
options.nextDue = true;
day = moment('2017-05-01').toDate();
dailyTask.frequency = 'weekly';
dailyTask.everyX = 1;
dailyTask.repeat = {
su: true,
m: true,
t: true,
w: true,
th: true,
f: true,
s: true,
};
dailyTask.startDate = day;
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-05-02').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-05-03').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2017-05-04').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2017-05-05').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2017-05-06').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2017-05-07').toDate());
dailyTask.everyX = 2;
dailyTask.repeat = {
su: true,
m: false,
t: false,
w: false,
th: false,
f: true,
s: false,
};
dailyTask.startDate = day;
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-05-05').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-05-14').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2017-05-19').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2017-05-28').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2017-06-02').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2017-06-11').toDate());
});
it('should not go into an infinite loop with invalid values', () => {
options.nextDue = true;
day = moment('2017-05-01').toDate();
dailyTask.frequency = 'weekly';
dailyTask.everyX = 1;
dailyTask.startDate = null;
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue).to.eql(false);
dailyTask.startDate = day;
dailyTask.everyX = 0;
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue).to.eql(false);
});
context('Day of the week matches', () => {
const weekdayMap = {
1: 'm',
@ -626,8 +713,9 @@ describe('shouldDo', () => {
it('leaves daily inactive if not day of the month', () => {
dailyTask.everyX = 1;
dailyTask.frequency = 'monthly';
dailyTask.daysOfMonth = [15];
let tomorrow = moment().add(1, 'day').toDate();// @TODO: make sure this is not the 15
let today = moment();
dailyTask.daysOfMonth = [today.date()];
let tomorrow = today.add(1, 'day').toDate();
expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
});
@ -645,8 +733,9 @@ describe('shouldDo', () => {
it('leaves daily inactive if not on date of the x month', () => {
dailyTask.everyX = 2;
dailyTask.frequency = 'monthly';
dailyTask.daysOfMonth = [15];
let tomorrow = moment().add(2, 'months').add(1, 'day').toDate();
let today = moment();
dailyTask.daysOfMonth = [today.date()];
let tomorrow = today.add(2, 'months').add(1, 'day').toDate();
expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
});
@ -667,6 +756,96 @@ describe('shouldDo', () => {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
it('should compute monthly nextDue values', () => {
options.timezoneOffset = 0;
options.nextDue = true;
day = moment('2017-05-01').toDate();
dailyTask.frequency = 'monthly';
dailyTask.everyX = 3;
dailyTask.startDate = day;
dailyTask.daysOfMonth = [1];
dailyTask.weeksOfMonth = [];
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-08-01').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-11-01').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2018-02-01').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2018-05-01').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2018-08-01').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2018-11-01').toDate());
dailyTask.daysOfMonth = [];
dailyTask.weeksOfMonth = [0];
dailyTask.everyX = 1;
dailyTask.repeat = {
su: false,
m: true,
t: false,
w: false,
th: false,
f: false,
s: false,
};
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-06-05').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-07-03').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2017-08-07').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2017-09-04').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2017-10-02').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2017-11-06').toDate());
day = moment('2017-05-08').toDate();
dailyTask.daysOfMonth = [];
dailyTask.weeksOfMonth = [1];
dailyTask.startDate = day;
dailyTask.everyX = 1;
dailyTask.repeat = {
su: false,
m: true,
t: false,
w: false,
th: false,
f: false,
s: false,
};
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-06-12').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-07-10').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2017-08-14').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2017-09-11').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2017-10-09').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2017-11-13').toDate());
day = moment('2017-05-29').toDate();
dailyTask.daysOfMonth = [];
dailyTask.weeksOfMonth = [4];
dailyTask.startDate = day;
dailyTask.everyX = 1;
dailyTask.repeat = {
su: false,
m: true,
t: false,
w: false,
th: false,
f: false,
s: false,
};
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2017-07-31').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2017-10-30').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2018-01-29').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2018-04-30').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2018-07-30').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2018-10-29').toDate());
});
context('Custom Day Start is 0 <= n < 24', () => {
beforeEach(() => {
options.dayStart = 7;
@ -734,6 +913,28 @@ describe('shouldDo', () => {
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
it('returns false when next due is requested and no repeats are available', () => {
dailyTask.repeat = {
su: false,
s: false,
f: false,
th: false,
w: false,
t: false,
m: false,
};
let today = moment('2017-05-27T17:34:40.000Z');
let week = today.monthWeek();
dailyTask.startDate = today.toDate();
dailyTask.weeksOfMonth = [week];
dailyTask.everyX = 1;
dailyTask.frequency = 'monthly';
day = moment('2017-02-23');
options.nextDue = true;
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
it('activates Daily if correct week of the month on the day of the start date', () => {
dailyTask.repeat = {
su: false,
@ -918,6 +1119,25 @@ describe('shouldDo', () => {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
it('should compute yearly nextDue values', () => {
options.timezoneOffset = 0;
options.nextDue = true;
day = moment('2017-05-01').toDate();
dailyTask.frequency = 'yearly';
dailyTask.everyX = 5;
dailyTask.startDate = day;
nextDue = shouldDo(day, dailyTask, options);
expect(nextDue.length).to.eql(6);
expect(moment(nextDue[0]).toDate()).to.eql(moment.utc('2022-05-01').toDate());
expect(moment(nextDue[1]).toDate()).to.eql(moment.utc('2027-05-01').toDate());
expect(moment(nextDue[2]).toDate()).to.eql(moment.utc('2032-05-01').toDate());
expect(moment(nextDue[3]).toDate()).to.eql(moment.utc('2037-05-01').toDate());
expect(moment(nextDue[4]).toDate()).to.eql(moment.utc('2042-05-01').toDate());
expect(moment(nextDue[5]).toDate()).to.eql(moment.utc('2047-05-01').toDate());
});
context('Custom Day Start is 0 <= n < 24', () => {
beforeEach(() => {
options.dayStart = 7;

View file

@ -2,7 +2,7 @@
## Babel Paths for Production Environment
In development, we [transpile at server start](https://github.com/HabitRPG/habitrpg/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
In development, we [transpile at server start](https://github.com/HabitRPG/habitica/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
This system means that requiring any files from `website/common/script` in `website/server/**/*.js` must be done through the `website/common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).

View file

@ -10,7 +10,7 @@ module.exports = {
index: path.resolve(__dirname, '../../dist-client/index.html'),
assetsRoot: path.resolve(__dirname, '../../dist-client'),
assetsSubDirectory: 'static',
assetsPublicPath: '/new-app',
assetsPublicPath: '/new-app/',
staticAssetsDirectory,
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as

View file

@ -83,7 +83,7 @@ const baseConfig = {
},
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
test: /\.(png|jpe?g|gif)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
@ -98,6 +98,22 @@ const baseConfig = {
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
},
},
{
test: /\.svg$/,
use: [
{ loader: 'svg-inline-loader' },
{ loader: 'svgo-loader' },
],
exclude: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
},
{
test: /\.svg$/,
use: [
{ loader: 'svg-url-loader' },
{ loader: 'svgo-loader' },
],
include: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
},
],
},
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,9 +1,9 @@
/* Comment out for holiday events */
.npc_ian {
/* .npc_ian {
background: url("/npc_ian.gif") no-repeat;
width: 78px;
height: 135px;
}
} */
.quest_burnout {
background: url("/quest_burnout.gif") no-repeat;
@ -61,6 +61,10 @@
padding-right: 0.5em;
}
.achievement-container {
height: 52px;
}
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */

View file

@ -6,25 +6,25 @@
}
.promo_backgrounds_armoire_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -847px -1042px;
background-position: -563px -1042px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -705px -1042px;
background-position: -847px -1042px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1042px;
background-position: -282px -1042px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -141px -1042px;
background-position: -1150px 0px;
width: 140px;
height: 441px;
}
@ -72,31 +72,31 @@
}
.promo_backgrounds_armoire_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1432px -442px;
background-position: 0px -1042px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px -148px;
background-position: -568px -600px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px -148px;
background-position: -840px -148px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201704 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -710px -600px;
background-position: -982px -148px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201705 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -568px -600px;
background-position: -710px -600px;
width: 141px;
height: 441px;
}
@ -132,7 +132,7 @@
}
.promo_checkin_incentives {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -563px -1042px;
background-position: -705px -1042px;
width: 141px;
height: 294px;
}
@ -162,10 +162,16 @@
}
.promo_contrib_spotlight_Keith {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1712px -1477px;
background-position: -1573px -903px;
width: 87px;
height: 111px;
}
.promo_contrib_spotlight_alys {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1803px -1365px;
width: 90px;
height: 110px;
}
.promo_contrib_spotlight_beffymaroo {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1712px -806px;
@ -174,19 +180,19 @@
}
.promo_contrib_spotlight_blade {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1803px -1365px;
background-position: -1573px -791px;
width: 89px;
height: 111px;
}
.promo_contrib_spotlight_cantras {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -791px;
background-position: -1573px -1124px;
width: 87px;
height: 109px;
}
.promo_contrib_spotlight_dewines {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1800px -1477px;
background-position: -1573px -1015px;
width: 89px;
height: 108px;
}
@ -204,7 +210,7 @@
}
.promo_cow {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -282px -1042px;
background-position: -1150px -442px;
width: 140px;
height: 441px;
}
@ -216,7 +222,7 @@
}
.promo_dilatoryDistress {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -637px -1632px;
background-position: -1183px -1632px;
width: 90px;
height: 90px;
}
@ -246,7 +252,7 @@
}
.promo_enchanted_armoire_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -364px -1632px;
background-position: -91px -1632px;
width: 90px;
height: 90px;
}
@ -258,7 +264,7 @@
}
.promo_enchanted_armoire_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -819px -1632px;
background-position: -546px -1632px;
width: 90px;
height: 90px;
}
@ -276,7 +282,7 @@
}
.promo_ghost_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1150px 0px;
background-position: -141px -1042px;
width: 140px;
height: 441px;
}
@ -294,7 +300,7 @@
}
.promo_habitoween_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1150px -442px;
background-position: -1432px -442px;
width: 140px;
height: 441px;
}
@ -330,13 +336,13 @@
}
.promo_mystery_201405 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -273px -1632px;
background-position: -455px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -1319px;
background-position: -1150px -884px;
width: 90px;
height: 96px;
}
@ -348,13 +354,13 @@
}
.promo_mystery_201408 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1260px -1337px;
background-position: -1824px -1477px;
width: 60px;
height: 71px;
}
.promo_mystery_201409 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -728px -1632px;
background-position: 0px -1632px;
width: 90px;
height: 90px;
}
@ -366,7 +372,7 @@
}
.promo_mystery_201411 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1001px -1632px;
background-position: -273px -1632px;
width: 90px;
height: 90px;
}
@ -378,31 +384,31 @@
}
.promo_mystery_201501 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1661px -791px;
background-position: -1712px -1568px;
width: 48px;
height: 63px;
}
.promo_mystery_201502 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -91px -1632px;
background-position: -819px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1579px -1484px;
background-position: -1001px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1321px -1337px;
background-position: -1260px -1337px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1397px -1484px;
background-position: -1274px -1632px;
width: 90px;
height: 90px;
}
@ -414,31 +420,31 @@
}
.promo_mystery_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -1113px;
background-position: -1573px -1340px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1367px -1190px;
background-position: -1306px -1484px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1632px;
background-position: -182px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1432px -884px;
background-position: -1494px -1484px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -182px -1632px;
background-position: -364px -1632px;
width: 90px;
height: 90px;
}
@ -456,49 +462,49 @@
}
.promo_mystery_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -455px -1632px;
background-position: -637px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -546px -1632px;
background-position: -728px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1291px -884px;
background-position: -1461px -1190px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1306px -1484px;
background-position: -910px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201606 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -1007px;
background-position: -1432px -884px;
width: 90px;
height: 105px;
}
.promo_mystery_201607 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -910px -1632px;
background-position: -1092px -1632px;
width: 90px;
height: 90px;
}
.promo_mystery_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1461px -1190px;
background-position: -1400px -1484px;
width: 93px;
height: 90px;
}
.promo_mystery_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1150px -884px;
background-position: -1367px -1190px;
width: 93px;
height: 90px;
}
@ -510,7 +516,7 @@
}
.promo_mystery_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -1219px;
background-position: -1291px -884px;
width: 90px;
height: 99px;
}
@ -522,7 +528,7 @@
}
.promo_mystery_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -901px;
background-position: -1573px -1234px;
width: 90px;
height: 105px;
}
@ -534,19 +540,25 @@
}
.promo_mystery_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1272px -1042px;
background-position: -989px -1042px;
width: 282px;
height: 147px;
}
.promo_mystery_201704 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1488px -1484px;
background-position: -1588px -1484px;
width: 90px;
height: 90px;
}
.promo_mystery_201705 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -989px -1042px;
background-position: -1272px -1042px;
width: 282px;
height: 147px;
}
.promo_mystery_201706 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1712px -1477px;
width: 111px;
height: 90px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 KiB

After

Width:  |  Height:  |  Size: 984 KiB

View file

@ -1,402 +1,426 @@
.promo_mystery_3014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -951px -699px;
background-position: -615px -1517px;
width: 217px;
height: 90px;
}
.promo_new_hair_fall2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -813px;
background-position: -707px -927px;
width: 140px;
height: 441px;
}
.promo_orca {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1208px -704px;
background-position: -1547px -1026px;
width: 105px;
height: 105px;
}
.promo_partyhats {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px -1200px;
background-position: -1667px -1459px;
width: 115px;
height: 47px;
}
.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1103px -1255px;
background-position: -983px -1369px;
width: 330px;
height: 83px;
}
.customize-option.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1128px -1270px;
background-position: -1008px -1384px;
width: 60px;
height: 60px;
}
.promo_pastel_skin_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -965px -813px;
background-position: -848px -1078px;
width: 354px;
height: 147px;
}
.customize-option.promo_pastel_skin_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -990px -828px;
background-position: -873px -1093px;
width: 60px;
height: 60px;
}
.promo_peppermint_flame {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -1075px;
background-position: -1254px -589px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -1223px;
background-position: -1249px -927px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1646px -1238px;
background-position: -1274px -942px;
width: 60px;
height: 60px;
}
.promo_pyromancer {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1208px -590px;
background-position: -1547px -875px;
width: 113px;
height: 113px;
}
.promo_rainbow_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1093px -524px;
background-position: -1875px -1098px;
width: 92px;
height: 103px;
}
.promo_seasonal_shop_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -301px -1403px;
background-position: -1667px -446px;
width: 279px;
height: 147px;
}
.promo_shimmer_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -907px -1403px;
background-position: -1314px -1369px;
width: 330px;
height: 83px;
}
.promo_shimmer_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -951px -257px;
background-position: 0px -927px;
width: 141px;
height: 441px;
}
.promo_shinySeeds {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1208px 0px;
background-position: -142px -927px;
width: 141px;
height: 441px;
}
.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1063px -1139px;
background-position: 0px -1628px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1088px -1154px;
background-position: -25px -1643px;
width: 60px;
height: 60px;
}
.promo_spooky_sparkles_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px -573px;
background-position: -1667px -151px;
width: 140px;
height: 294px;
}
.promo_spring_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -740px -1255px;
background-position: -620px -1369px;
width: 362px;
height: 102px;
}
.promo_spring_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -871px -964px;
background-position: 0px -1369px;
width: 309px;
height: 147px;
}
.promo_springclasses2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -289px -1643px;
background-position: -1667px -1368px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1643px;
background-position: -326px -1517px;
width: 288px;
height: 90px;
}
.promo_staff_spotlight_Lemoness {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1491px -721px;
background-position: -1808px -299px;
width: 102px;
height: 146px;
}
.promo_staff_spotlight_Viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1491px -573px;
background-position: -1547px -573px;
width: 119px;
height: 147px;
}
.promo_staff_spotlight_paglias {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1208px -442px;
background-position: -1547px -724px;
width: 99px;
height: 147px;
}
.promo_steampunk_3017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -141px -813px;
background-position: -425px -927px;
width: 140px;
height: 441px;
}
.promo_summer_classes_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -310px -1255px;
background-position: -848px -1226px;
width: 429px;
height: 102px;
}
.promo_summer_classes_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1554px;
background-position: -1667px -1279px;
width: 300px;
height: 88px;
}
.promo_summer_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -564px -813px;
background-position: -848px -927px;
width: 400px;
height: 150px;
}
.promo_summer_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1254px 0px;
width: 141px;
height: 588px;
}
.promo_takeThis_gear {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1093px -257px;
background-position: -1547px -1177px;
width: 114px;
height: 87px;
}
.promo_takethis_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1093px -345px;
background-position: -1278px -1226px;
width: 114px;
height: 87px;
}
.promo_task_planning {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px -181px;
background-position: -1396px -181px;
width: 240px;
height: 195px;
}
.promo_turkey_day_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -282px -813px;
background-position: -284px -927px;
width: 140px;
height: 441px;
}
.promo_unconventional_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1778px -172px;
background-position: -1890px -1001px;
width: 60px;
height: 60px;
}
.promo_unconventional_armor2 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1772px -320px;
background-position: -1890px -926px;
width: 70px;
height: 74px;
}
.promo_updos {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -172px;
background-position: -1808px -151px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -340px -326px;
background-position: -986px -1517px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -499px -497px;
background-position: -932px -782px;
width: 360px;
height: 90px;
}
.promo_winter_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -397px -593px;
background-position: 0px -782px;
width: 432px;
height: 144px;
}
.promo_winter_fireworks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -1371px;
background-position: -1203px -1078px;
width: 138px;
height: 147px;
}
.promo_winterclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -581px -1403px;
background-position: 0px -1517px;
width: 325px;
height: 110px;
}
.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -813px;
background-position: -566px -927px;
width: 140px;
height: 441px;
}
.customize-option.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -25px -828px;
background-position: -591px -942px;
width: 60px;
height: 60px;
}
.promo_winteryhair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -340px -250px;
background-position: -833px -1517px;
width: 152px;
height: 75px;
}
.avatar_variety {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -564px -1139px;
background-position: -433px -782px;
width: 498px;
height: 95px;
}
.npc_viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1093px -433px;
background-position: -199px -1628px;
width: 108px;
height: 90px;
}
.party_preview {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -499px 0px;
background-position: 0px -562px;
width: 451px;
height: 219px;
}
.promo_backtoschool {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -773px;
background-position: -1396px -573px;
width: 150px;
height: 150px;
}
.promo_cooking {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -593px;
background-position: -452px -562px;
width: 396px;
height: 219px;
}
.promo_startingover {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -924px;
background-position: -1396px -875px;
width: 150px;
height: 150px;
}
.promo_valentines {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1255px;
background-position: -310px -1369px;
width: 309px;
height: 147px;
}
.promo_working_out {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1403px;
background-position: -1667px 0px;
width: 300px;
height: 150px;
}
.scene_arts_crafts {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -951px 0px;
background-position: -926px -277px;
width: 256px;
height: 256px;
}
.scene_buying_rewards {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1667px -1098px;
width: 207px;
height: 180px;
}
.scene_coding {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -622px;
background-position: -1396px -1177px;
width: 150px;
height: 150px;
}
.scene_dailies {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -499px -220px;
background-position: -926px 0px;
width: 327px;
height: 276px;
}
.scene_eco_friendly {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px 0px;
background-position: -1667px -926px;
width: 222px;
height: 171px;
}
.scene_guilds {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px 0px;
background-position: 0px -312px;
width: 498px;
height: 249px;
}
.scene_habitica_house {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px 0px;
width: 585px;
height: 311px;
}
.scene_habits {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -564px -964px;
background-position: -926px -534px;
width: 306px;
height: 174px;
}
.scene_meditation {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -320px;
background-position: -1396px -724px;
width: 150px;
height: 150px;
}
.scene_phone_peek {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1621px -471px;
background-position: -1396px -1026px;
width: 150px;
height: 150px;
}
.scene_raking_leaves {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -586px -343px;
width: 246px;
height: 198px;
}
.scene_todos {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px -377px;
background-position: -1396px -377px;
width: 240px;
height: 195px;
}
.scene_video_games {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -250px;
background-position: -586px 0px;
width: 339px;
height: 342px;
}
.welcome_basic_avatars {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px -868px;
background-position: -1667px -760px;
width: 246px;
height: 165px;
}
.welcome_promo_party {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px 0px;
background-position: -1396px 0px;
width: 270px;
height: 180px;
}
.welcome_sample_tasks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1350px -1034px;
background-position: -1667px -594px;
width: 246px;
height: 165px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 944 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 KiB

After

Width:  |  Height:  |  Size: 506 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 166 KiB

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more