Merge branch 'develop' into separate_mocha_tests

This commit is contained in:
Blade Barringer 2015-05-07 21:11:30 -05:00
commit ece37c01db
37 changed files with 4240 additions and 3676 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 283 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 KiB

After

Width:  |  Height:  |  Size: 536 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 339 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -87,5 +87,13 @@
"backgroundFloralMeadowText": "Flowering Meadow", "backgroundFloralMeadowText": "Flowering Meadow",
"backgroundFloralMeadowNotes": "Picnic in a Flowering Meadow.", "backgroundFloralMeadowNotes": "Picnic in a Flowering Meadow.",
"backgroundGumdropLandText": "Gumdrop Land", "backgroundGumdropLandText": "Gumdrop Land",
"backgroundGumdropLandNotes": "Nibble the scenery of Gumdrop Land." "backgroundGumdropLandNotes": "Nibble the scenery of Gumdrop Land.",
"backgrounds052015": "SET 12: Released May 2015",
"backgroundMarbleTempleText": "Marble Temple",
"backgroundMarbleTempleNotes": "Pose in front of a Marble Temple.",
"backgroundMountainLakeText": "Mountain Lake",
"backgroundMountainLakeNotes": "Dip your toes in a Mountain Lake.",
"backgroundPagodasText": "Pagodas",
"backgroundPagodasNotes": "Climb to the top of Pagodas."
} }

View file

@ -96,5 +96,9 @@
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.", "abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
"abuseAlreadyReported": "You have already reported this message.", "abuseAlreadyReported": "You have already reported this message.",
"needsText": "Please type a message.", "needsText": "Please type a message.",
"needsTextPlaceholder": "Type your message here." "needsTextPlaceholder": "Type your message here.",
"copyMessageAsToDo": "Copy message as To-Do",
"messageAddedAsToDo": "Message copied as To-Do.",
"messageWroteIn": "<%= user %> wrote in <%= group %>",
"msgPreviewHeading": "Message Preview"
} }

View file

@ -97,7 +97,7 @@
"invitedParty": "Invited To Party", "invitedParty": "Invited To Party",
"invitedGuild": "Invited To Guild", "invitedGuild": "Invited To Guild",
"inactivityEmails": "Your account is inactive", "inactivityEmails": "Your account is inactive",
"weeklyRecaps": "Summaries of your acccount activity in the past week", "weeklyRecaps": "Summaries of your account activity in the past week",
"questStarted": "Your Quest has Begun", "questStarted": "Your Quest has Begun",
"invitedQuest": "Invited to Quest", "invitedQuest": "Invited to Quest",
"kickedGroup": "Kicked from group", "kickedGroup": "Kicked from group",

View file

@ -1798,6 +1798,16 @@ api.backgrounds =
gumdrop_land: gumdrop_land:
text: t('backgroundGumdropLandText') text: t('backgroundGumdropLandText')
notes: t('backgroundGumdropLandNotes') notes: t('backgroundGumdropLandNotes')
backgrounds052015:
marble_temple:
text: t('backgroundMarbleTempleText')
notes: t('backgroundMarbleTempleNotes')
mountain_lake:
text: t('backgroundMountainLakeText')
notes: t('backgroundMountainLakeNotes')
pagodas:
text: t('backgroundPagodasText')
notes: t('backgroundPagodasNotes')
api.subscriptionBlocks = api.subscriptionBlocks =
basic_earned: months:1, price:5 basic_earned: months:1, price:5

View file

@ -1,5 +1,5 @@
#jslint node: true #jslint node: true
# @TODO: refactor tests
#global describe, before, beforeEach, it #global describe, before, beforeEach, it
"use strict" "use strict"
_ = require("lodash") _ = require("lodash")
@ -72,6 +72,14 @@ describe "API", ->
.set("X-API-Key", apiToken) .set("X-API-Key", apiToken)
cb null, res.body cb null, res.body
registerManyUsers = (number, callback) ->
async.times number, (n, next) ->
registerNewUser (err, user) ->
next(err, user)
, false
, (err, users) ->
callback(err, users)
before (done) -> before (done) ->
require "../website/src/server" # start the server require "../website/src/server" # start the server
# then wait for it to do it's thing. TODO make a cb-compatible export of server # then wait for it to do it's thing. TODO make a cb-compatible export of server
@ -227,7 +235,7 @@ describe "API", ->
).end (res) -> ).end (res) ->
expectCode res, 200 expectCode res, 200
body = res.body body = res.body
expect(body).to.be.empty expect(body).to.be.empty
done() done()
it "Does not delete already deleted todo", (done) -> it "Does not delete already deleted todo", (done) ->
@ -257,15 +265,127 @@ describe "API", ->
describe "Groups", -> describe "Groups", ->
group = undefined group = undefined
before (done) -> before (done) ->
request.post(baseURL + "/groups").send( request.post(baseURL + "/groups").send(
name: "TestGroup" name: "TestGroup"
type: "party" type: "party"
).end (res) -> ).end (res) ->
expectCode res, 200 expectCode res, 200
group = res.body group = res.body
expect(group.members.length).to.be 1 expect(group.members.length).to.be 1
expect(group.leader).to.be user._id expect(group.leader).to.be user._id
done() done()
describe "Guilds", ->
before (done) ->
User.findByIdAndUpdate user._id,
$set:
"balance": 4
, (err, _user) ->
done()
describe "Private Guilds", ->
guild = undefined
before (done) ->
request.post(baseURL + "/groups").send(
name: "TestPrivateGroup"
type: "guild"
privacy: "private"
).end (res) ->
expectCode res, 200
guild = res.body
expect(guild.members.length).to.be 1
expect(guild.leader).to.be user._id
#Add members to guild
async.waterfall [
(cb) ->
registerManyUsers 15, cb
(_members, cb) ->
members = _members
joinGuild = (member, callback) ->
request.post(baseURL + "/groups/" + guild._id + "/join")
.set("X-API-User", member._id)
.set("X-API-Key", member.apiToken)
.end ->
callback(null, null)
async.map members, joinGuild, (err, results) -> cb()
], done
it "includes user in private group member list when user is a member", (done) ->
request.get(baseURL + "/groups/" + guild._id)
.end (res) ->
g = res.body
userInGroup = _.find g.members, (member) -> return member._id == user._id
expect(userInGroup).to.not.be undefined
done()
it "excludes user from viewing private group member list when user is not a member", (done) ->
request.post(baseURL + "/groups/" + guild._id + "/leave")
.end (res) ->
request.get(baseURL + "/groups/" + guild._id)
.end (res) ->
expect res, 404
done()
describe "Public Guilds", ->
guild = undefined
before (done) ->
request.post(baseURL + "/groups").send(
name: "TestPublicGroup"
type: "guild"
privacy: "public"
).end (res) ->
expectCode res, 200
guild = res.body
expect(guild.members.length).to.be 1
expect(guild.leader).to.be user._id
#Add members to guild
async.waterfall [
(cb) ->
registerManyUsers 15, cb
(_members, cb) ->
members = _members
joinGuild = (member, callback) ->
request.post(baseURL + "/groups/" + guild._id + "/join")
.set("X-API-User", member._id)
.set("X-API-Key", member.apiToken)
.end ->
callback(null, null)
async.map members, joinGuild, (err, results) -> cb()
], done
it "includes user in public group member list when user is a member", (done) ->
request.get(baseURL + "/groups/" + guild._id)
.end (res) ->
g = res.body
expect(g.members.length).to.be 15
userInGroup = _.find g.members, (member) -> return member._id == user._id
expect(userInGroup).to.not.be undefined
done()
it "excludes user in public group member list when user is not a member", (done) ->
#Remove user from group
request.post(baseURL + "/groups/" + guild._id + "/leave")
.end (res) ->
#Verfiy that when a user query's for a group they are in the group if they are a member
request.get(baseURL + "/groups/" + guild._id)
.end (res) ->
g = res.body
expect(g.members.length).to.be 15
userInGroup = _.find g.members, (member) -> return member._id == user._id
expect(userInGroup).to.be undefined
done()
describe "Party", -> describe "Party", ->
it "can be found by querying for party", (done) -> it "can be found by querying for party", (done) ->
@ -273,7 +393,6 @@ describe "API", ->
type: "party" type: "party"
).end (res) -> ).end (res) ->
expectCode res, 200 expectCode res, 200
party = res.body[0] party = res.body[0]
expect(party._id).to.be group._id expect(party._id).to.be group._id
expect(party.leader).to.be user._id expect(party.leader).to.be user._id
@ -282,6 +401,133 @@ describe "API", ->
expect(party.memberCount).to.be group.memberCount expect(party.memberCount).to.be group.memberCount
done() done()
it "includes user in a party member list when user is a member", (done) ->
party = []
#Invite some members
async.waterfall [
# Register new users
(cb) ->
registerManyUsers 15, cb
# Send them invitations
(_party, cb) ->
party = _party
joinParty = (member, callback) ->
request.post(baseURL + "/groups/" + group._id + "/join")
.set("X-API-User", member._id)
.set("X-API-Key", member.apiToken)
.end ->
callback(null, null)
async.map party, joinParty, (err, results) -> cb()
# Accept / Reject
(cb) ->
# series since they'll be modifying the same group record
series = _.reduce(party, (m, v, i) ->
m.push (cb2) ->
request.post(baseURL + "/groups/" + group._id + "/join")
.set("X-API-User", party[i]._id)
.set("X-API-Key", party[i].apiToken)
.end ->
cb2()
m
, [])
async.series series, cb
# Make sure the invites stuck
(result, cb) ->
request.get(baseURL + "/groups/" + group._id)
.send()
.end (res) ->
g = res.body
userInGroup = _.find g.members, (member) -> return member._id == user._id
expect(userInGroup).to.not.be undefined
cb()
# Remove all previous members
(cb) ->
joinParty = (member, callback) ->
request.post(baseURL + "/groups/" + group._id + "/leave")
.set("X-API-User", member._id)
.set("X-API-Key", member.apiToken)
.end ->
callback(null, null)
async.map party, joinParty, (err, results) -> cb()
], done
it "excludes user in a party member list when user is not a member", (done) ->
party = []
#Invite some members
async.waterfall [
# Register new users
(cb) ->
registerManyUsers 15, cb
# Send them invitations
(_party, cb) ->
party = _party
joinParty = (member, callback) ->
request.post(baseURL + "/groups/" + group._id + "/join")
.set("X-API-User", member._id)
.set("X-API-Key", member.apiToken)
.end ->
callback(null, null)
async.map party, joinParty, (err, results) -> cb()
# Accept / Reject
(cb) ->
# series since they'll be modifying the same group record
series = _.reduce(party, (m, v, i) ->
m.push (cb2) ->
request.post(baseURL + "/groups/" + group._id + "/join")
.set("X-API-User", party[i]._id)
.set("X-API-Key", party[i].apiToken)
.end ->
cb2()
m
, [])
async.series series, cb
(result, cb) ->
#Remove a user from group
request.post(baseURL + "/groups/" + group._id + "/leave")
.set("X-API-User", party[0]._id)
.set("X-API-Key", party[0].apiToken)
.send()
.end (res) ->
request.get(baseURL + "/groups/" + group._id)
.set("X-API-User", party[0]._id)
.set("X-API-Key", party[0].apiToken)
.send()
.end (res) ->
expect res, 404
cb()
# Remove all previous members
(cb) ->
joinParty = (member, callback) ->
request.post(baseURL + "/groups/" + group._id + "/leave")
.set("X-API-User", member._id)
.set("X-API-Key", member.apiToken)
.end ->
callback(null, null)
async.map party, joinParty, (err, results) -> cb()
], done
describe "Chat", -> describe "Chat", ->
chat = undefined chat = undefined
it "Posts a message to party chat", (done) -> it "Posts a message to party chat", (done) ->
@ -527,14 +773,7 @@ describe "API", ->
# Register new users # Register new users
(cb) -> (cb) ->
async.parallel [ registerManyUsers 3, cb
(cb2) ->
registerNewUser cb2, false
(cb2) ->
registerNewUser cb2, false
(cb2) ->
registerNewUser cb2, false
], cb
# Send them invitations # Send them invitations
(_party, cb) -> (_party, cb) ->
@ -612,7 +851,7 @@ describe "API", ->
party[0] = res.body party[0] = res.body
request.post(baseURL + "/user/class/cast/snowball?targetType=user&targetId=" + party[0]._id).end (res) -> request.post(baseURL + "/user/class/cast/snowball?targetType=user&targetId=" + party[0]._id).end (res) ->
#expect(res.body.stats.mp).to.be.below(mp); #expect(res.body.stats.mp).to.be.below(mp)
request.get(baseURL + "/members/" + party[0]._id).end (res) -> request.get(baseURL + "/members/" + party[0]._id).end (res) ->
member = res.body member = res.body
expect(member.achievements.snowball).to.be 1 expect(member.achievements.snowball).to.be 1
@ -747,8 +986,8 @@ describe "API", ->
cummGp = shared.content.quests.vice3.drop.gp + shared.content.quests.dilatory.drop.gp cummGp = shared.content.quests.vice3.drop.gp + shared.content.quests.dilatory.drop.gp
#//FIXME check that user got exp, but user is leveling up making the exp check difficult #//FIXME check that user got exp, but user is leveling up making the exp check difficult
# expect(user.stats.exp).to.be.above(cummExp); # expect(user.stats.exp).to.be.above(cummExp)
# expect(user.stats.gp).to.be.above(cummGp); # expect(user.stats.gp).to.be.above(cummGp)
async.parallel [ async.parallel [
# Tavern Boss # Tavern Boss

View file

@ -68,6 +68,76 @@ describe('Groups Controller', function() {
}); });
}); });
describe("Chat Controller", function() {
var scope, ctrl, user, $rootScope, $controller;
beforeEach(function() {
module(function($provide) {
$provide.value('User', {});
});
inject(function(_$rootScope_, _$controller_){
user = specHelper.newUser();
user._id = "unique-user-id";
$rootScope = _$rootScope_;
scope = _$rootScope_.$new();
$controller = _$controller_;
// Load RootCtrl to ensure shared behaviors are loaded
$controller('RootCtrl', {$scope: scope, User: {user: user}});
ctrl = $controller('ChatCtrl', {$scope: scope});
});
});
describe('copyToDo', function() {
it('when copying a user message it opens modal with information from message', function() {
scope.group = {
name: "Princess Bride"
};
var modalSpy = sinon.spy($rootScope, "openModal");
var message = {
uuid: 'the-dread-pirate-roberts',
user: 'Wesley',
text: 'As you wish'
};
scope.copyToDo(message);
modalSpy.should.have.been.calledOnce;
modalSpy.should.have.been.calledWith('copyChatToDo', sinon.match(function(callArgToMatch){
return callArgToMatch.controller == 'CopyMessageModalCtrl'
&& callArgToMatch.scope.text == message.text
}));
});
it('when copying a system message it opens modal with information from message', function() {
scope.group = {
name: "Princess Bride"
};
var modalSpy = sinon.spy($rootScope, "openModal");
var message = {
uuid: 'system',
text: 'Wesley attacked the ROUS in the Fire Swamp'
};
scope.copyToDo(message);
modalSpy.should.have.been.calledOnce;
modalSpy.should.have.been.calledWith('copyChatToDo', sinon.match(function(callArgToMatch){
return callArgToMatch.controller == 'CopyMessageModalCtrl'
&& callArgToMatch.scope.text == message.text
}));
});
});
});
describe("Autocomplete controller", function() { describe("Autocomplete controller", function() {
var scope, ctrl, user, $rootScope, $controller; var scope, ctrl, user, $rootScope, $controller;
@ -172,3 +242,57 @@ describe("Autocomplete controller", function() {
}); });
}); });
}); });
describe("CopyMessageModal controller", function() {
var scope, ctrl, user, Notification, $rootScope, $controller;
beforeEach(function() {
module(function($provide) {
$provide.value('User', {});
});
inject(function($rootScope, _$controller_, _Notification_){
user = specHelper.newUser();
user._id = "unique-user-id";
user.ops = {
addTask: sinon.spy()
};
scope = $rootScope.$new();
scope.$close = sinon.spy();
$controller = _$controller_;
// Load RootCtrl to ensure shared behaviors are loaded
$controller('RootCtrl', {$scope: scope, User: {user: user}});
ctrl = $controller('CopyMessageModalCtrl', {$scope: scope, User: {user: user}});
Notification = _Notification_;
Notification.text = sinon.spy();
});
});
describe("saveTodo", function() {
it('saves todo', function() {
scope.text = "A Tavern msg";
scope.notes = "Some notes";
var payload = {
body: {
text: scope.text,
type: 'todo',
notes: scope.notes
}
};
scope.saveTodo();
user.ops.addTask.should.have.been.calledOnce;
user.ops.addTask.should.have.been.calledWith(payload);
Notification.text.should.have.been.calledOnce;
Notification.text.should.have.been.calledWith(window.env.t('messageAddedAsToDo'));
scope.$close.should.have.been.calledOnce;
});
});
});

View file

@ -187,3 +187,6 @@ a.label
color: #fff !important color: #fff !important
.line-through .line-through
text-decoration line-through text-decoration line-through
.markdown-preview markdown code
white-space inherit

View file

@ -142,6 +142,14 @@ for $stage in $stages
padding: 0 padding: 0
font-weight: 300 font-weight: 300
.task-column.preview
padding: 0
background: transparent
border: 0;
.task:hover
cursor: auto
// 50% width columns with scrollbars for tablets // 50% width columns with scrollbars for tablets
@media (min-width: 768px) and (max-width: 970px) @media (min-width: 768px) and (max-width: 970px)
.task-column .task-column

View file

@ -15,11 +15,13 @@ angular.module('habitrpg')
var runAuth = function(id, token) { var runAuth = function(id, token) {
User.authenticate(id, token, function(err) { User.authenticate(id, token, function(err) {
if(!err) $scope.registrationInProgress = false;
$window.location.href = ('/' + window.location.hash); $window.location.href = ('/' + window.location.hash);
}); });
}; };
function errorAlert(data, status, headers, config) { function errorAlert(data, status, headers, config) {
$scope.registrationInProgress = false;
if (status === 0) { if (status === 0) {
$window.alert(window.env.t('noReachServer')); $window.alert(window.env.t('noReachServer'));
} else if (!!data && !!data.err) { } else if (!!data && !!data.err) {
@ -29,6 +31,8 @@ angular.module('habitrpg')
} }
}; };
$scope.registrationInProgress = false;
$scope.register = function() { $scope.register = function() {
/*TODO highlight invalid inputs /*TODO highlight invalid inputs
we have this as a workaround for https://github.com/HabitRPG/habitrpg-mobile/issues/64 we have this as a workaround for https://github.com/HabitRPG/habitrpg-mobile/issues/64
@ -36,6 +40,8 @@ angular.module('habitrpg')
var scope = angular.element(document.getElementById('registrationForm')).scope(); var scope = angular.element(document.getElementById('registrationForm')).scope();
if (scope.registrationForm.$invalid) return; if (scope.registrationForm.$invalid) return;
$scope.registrationInProgress = true;
var url = ApiUrl.get() + "/api/v2/register"; var url = ApiUrl.get() + "/api/v2/register";
if($rootScope.selectedLanguage) url = url + '?lang=' + $rootScope.selectedLanguage.code; if($rootScope.selectedLanguage) url = url + '?lang=' + $rootScope.selectedLanguage.code;
$http.post(url, scope.registerVals).success(function(data, status, headers, config) { $http.post(url, scope.registerVals).success(function(data, status, headers, config) {

View file

@ -362,7 +362,25 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}); });
}); });
} }
} };
$scope.copyToDo = function(message) {
var taskNotes = env.t("messageWroteIn", {
user: message.uuid == 'system'
? 'system'
: '[' + message.user + '](' + env.BASE_URL + '/static/front/#?memberId=' + message.uuid + ')',
group: '[' + $scope.group.name + '](' + window.location.href + ')'
});
var newScope = $scope.$new();
newScope.text = message.text;
newScope.notes = taskNotes;
$rootScope.openModal('copyChatToDo',{
controller:'CopyMessageModalCtrl',
scope: newScope
});
};
$scope.sync = function(group){ $scope.sync = function(group){
group.$get(); group.$get();
@ -567,3 +585,20 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
} }
} }
]) ])
.controller("CopyMessageModalCtrl", ['$scope', 'User', 'Notification',
function($scope, User, Notification){
$scope.saveTodo = function() {
var newTask = {
text: $scope.text,
type: 'todo',
notes: $scope.notes
};
User.user.ops.addTask({body:newTask});
Notification.text(window.env.t('messageAddedAsToDo'));
$scope.$close();
}
}
]);

View file

@ -217,6 +217,7 @@ function($rootScope, User, $timeout, $state) {
}); });
var goto = function(chapter, page, force) { var goto = function(chapter, page, force) {
return; // TODO temporarily remove old tutorial system while experimenting with leslie's new gettup
var curr = User.user.flags.tour[chapter]; var curr = User.user.flags.tour[chapter];
if (page != curr+1 && !force) return; if (page != curr+1 && !force) return;
var updates = {};updates['flags.tour.'+chapter] = page; var updates = {};updates['flags.tour.'+chapter] = page;

View file

@ -137,7 +137,23 @@ api.get = function(req, res, next) {
q.exec(function(err, group){ q.exec(function(err, group){
if (err) return next(err); if (err) return next(err);
if (!group && gid!=='party') return res.json(404,{err: "Group not found or you don't have access."}); if (!group && gid!=='party') return res.json(404,{err: "Group not found or you don't have access."});
res.json(group); //Since we have a limit on how many members are populate to the group, we want to make sure the user is always in the group
var userInGroup = _.find(group.members, function(member){ return member._id == user._id; });
//If the group is private or the group is a party, then the user must be a member of the group based on access restrictions above
if (group.privacy === 'private' || gid === 'party') {
//If the user is not in the group query, add them
if (!userInGroup) { group.members.push(user); }
res.json(group);
} else if ( group.privacy === "public" ) { //The group is public, we must do an extra check to see if the user is already in the group query
//We must see how to check if a user is a member of a public group, so we requery
var q2 = Group.findOne({ _id: group._id, privacy:'public', members: {$in:[user._id]} });
q2.exec(function(err, group2){
if (err) return next(err);
if (group2 && !userInGroup) { group.members.push(user); }
res.json(group);
});
}
gid = null; gid = null;
}); });
}; };
@ -398,7 +414,8 @@ api.likeChatMessage = function(req, res, next) {
api.join = function(req, res, next) { api.join = function(req, res, next) {
var user = res.locals.user, var user = res.locals.user,
group = res.locals.group; group = res.locals.group,
isUserInvited = false;
if (group.type == 'party' && group._id == (user.invitations && user.invitations.party && user.invitations.party.id)) { if (group.type == 'party' && group._id == (user.invitations && user.invitations.party && user.invitations.party.id)) {
User.update({_id:user.invitations.party.inviter}, {$inc:{'items.quests.basilist':1}}).exec(); // Reward inviter User.update({_id:user.invitations.party.inviter}, {$inc:{'items.quests.basilist':1}}).exec(); // Reward inviter
@ -409,16 +426,25 @@ api.join = function(req, res, next) {
group.quest.members[user._id] = undefined; group.quest.members[user._id] = undefined;
group.markModified('quest.members'); group.markModified('quest.members');
} }
} isUserInvited = true;
else if (group.type == 'guild' && user.invitations && user.invitations.guilds) { } else if (group.type == 'guild' && user.invitations && user.invitations.guilds) {
var i = _.findIndex(user.invitations.guilds, {id:group._id}); var i = _.findIndex(user.invitations.guilds, {id:group._id});
if (~i) user.invitations.guilds.splice(i,1); if (~i){
user.save(); isUserInvited = true;
user.invitations.guilds.splice(i,1);
user.save();
}else{
isUserInvited = group.privacy === 'private' ? false : true;
}
} }
if(!isUserInvited) return res.json(401, {err: "Can't join a group you're not invited to."});
if (!_.contains(group.members, user._id)){ if (!_.contains(group.members, user._id)){
group.members.push(user._id); group.members.push(user._id);
group.invites.splice(_.indexOf(group.invites, user._id), 1); if (group.invites.length > 0) {
group.invites.splice(_.indexOf(group.invites, user._id), 1);
}
} }
async.series([ async.series([
@ -430,7 +456,6 @@ api.join = function(req, res, next) {
} }
], function(err, results){ ], function(err, results){
if (err) return next(err); if (err) return next(err);
// Return the group? Or not? // Return the group? Or not?
res.json(results[1]); res.json(results[1]);
group = null; group = null;

View file

@ -31,6 +31,9 @@ mixin chatMessages(inbox)
| &nbsp; &nbsp; | &nbsp; &nbsp;
a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")') a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")')
span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""') span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""')
| &nbsp; &nbsp;
a(ng-click="copyToDo(message)")
span.glyphicon.glyphicon-share(tooltip=env.t('copyMessageAsToDo'))
span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""') span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""')
a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')

View file

@ -113,3 +113,33 @@ script(type='text/ng-template', id='partials/options.social.html')
.tab-content .tab-content
.tab-pane.active .tab-pane.active
div(ui-view) div(ui-view)
script(type='text/ng-template', id='modals/copyChatToDo.html')
.modal-header
h4=env.t('copyMessageAsToDo')
.modal-body
.form-group
input.form-control(type='text',ng-model='text', ng-model-options="{debounce: 1000}")
.form-group
textarea.form-control(rows='5',ng-model='notes', ng-model-options="{debounce: 1000}", focus-me)
hr
div.task-column.preview
div(ng-init='popoverOpen = false', class='task todo uncompleted color-neutral', popover-trigger='mouseenter', data-popover-html="{{popoverOpen ? '' : notes | markdown}}", popover-placement="top")
// right-hand side control buttons
.task-meta-controls
// Icons only available if you own the tasks (aka, hidden from challenge stats)
span(ng-if='!obj._locked')
// notes
span.task-notes(ng-show='notes', ng-click='popoverOpen = !popoverOpen', popover-trigger='click', data-popover-html="{{notes | markdown}}", popover-placement="top")
span.glyphicon.glyphicon-comment
| &nbsp;
// main content
div.task-text
markdown(text='text',target='_blank')
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('close')
button.btn.btn-primary(ng-click='saveTodo()')=env.t('submit')

View file

@ -11,3 +11,9 @@ mixin aLink(url, label)
a(href="", ng-click="externalLink('#{url}')")= label a(href="", ng-click="externalLink('#{url}')")= label
else else
a(href='#{url}', target='_blank')= label a(href='#{url}', target='_blank')= label
mixin previewMarkdown(text)
.panel.panel-warning
.panel-heading=env.t('msgPreviewHeading')
.panel-body.markdown-preview
markdown(text='#{text}')

View file

@ -1,36 +1,44 @@
h5 5/01/2015 - Skills Rebalancing h5 5/06/2015 - MAY BACKGROUNDS REVEALED
hr hr
tr tr
td td
p After over a year's worth of complaints on Trello and countless bug reports on Github, and after almost five months of dedicated debating, coding, and testing, we have finally rolled out an overhaul of the skills and mana system. Thank you so much to everyone who contributed opinions, time, and effort! Here's what happened: .background_mountain_lake.pull-right
tr h5 May Backgrounds Revealed
td p There are three new avatar backgrounds in the <a href='https://habitrpg.com/#/options/profile/backgrounds' target='_blank'>Background Shop</a>! Now your avatar can climb to the top of Pagodas, pose in front of a Marble Temple, or dip toes in a Mountain Lake!
h5 HABITS DAMAGE BOSSES p.small.muted by (in order) Teto is Great, Painter de Cluster, and Hazel40
p Now it's even easier to damage a Boss without skills! It was definitely high time that HabitRPG counted Habits towards battles. We were not living up to our name.
tr
td
h5 MANA DIRECTLY LINKED TO PROGRESS
p Mana was pretty broken: you gained it overnight no matter what you'd done the day before, and it was only linked to To-Dos. This meant that using skills was pretty detached from how well you were doing in your real life, which defeated the purpose of the game!
p Now you can gain Mana from Dailies and positive Habits as well as To-Dos! You still gain Mana overnight, but how much you gain depends on how many of your Dailies you did.
p You can also lose Mana by hitting negative Habits. Yikes! Better not indulge...
tr
td
h5 SKILLS STRENGTHENED AND WEAKENED
p Some notoriously overpowered skills have been weakened, like Backstab (which often granted over 1000GP per cast), Burst of Flames (which allowed you to 1-hit KO most bosses regardless of how much or how little you'd done that day), and Valorous Presence (which caused the notorious "I checked off one Daily and gained 997 levels" bug reports).
p Some painfully UNDERpowered skills have been strengthened, such as Tools of the Trade and Searing Brightness. We think you'll like these much better now!
p For a full description of what changed, <a href='http://habitrpg.wikia.com/wiki/User_blog:LadyAlys/Skills_Rebalancing_and_Other_Changes' target='_blank'>check out this blog post</a>.
tr
td
h5 GIVING FEEDBACK
p We understand that these changes may be disruptive to some of you. We're sorry! We hope that with time, you will find them to be a drastic improvement. We will be waiting two weeks (so that everyone can get used to the new system), and then on May 15th will open up a new Trello card for comments and feedback.
p For those of you who want to reallocate stats now that you have the new system, please post your User ID in this <a href='https://github.com/HabitRPG/habitrpg/issues/5082' target='_blank'>Github ticket</a>. (Your UUID is found under Settings > API; do NOT post your API Token.) That ticket is also being used to debate a possibility to make it so that reallocating stats does not cost gems. If that's a feature you would like (or would hate), definitely drop by that ticket and let us know!
p.small.muted by Alys, Verabird, brandonjreid, ShilohT, betaveros, SabreCat, chimericdream, and everyone who commented with feedback on Trello and Github. You guys are great!
hr hr
a(href='/static/old-news', target='_blank') Read older news a(href='/static/old-news', target='_blank') Read older news
mixin oldNews mixin oldNews
h5 5/01/2015 - Skills Rebalancing
hr
tr
td
p After over a year's worth of complaints on Trello and countless bug reports on Github, and after almost five months of dedicated debating, coding, and testing, we have finally rolled out an overhaul of the skills and mana system. Thank you so much to everyone who contributed opinions, time, and effort! Here's what happened:
tr
td
h5 HABITS DAMAGE BOSSES
p Now it's even easier to damage a Boss without skills! It was definitely high time that HabitRPG counted Habits towards battles. We were not living up to our name.
tr
td
h5 MANA DIRECTLY LINKED TO PROGRESS
p Mana was pretty broken: you gained it overnight no matter what you'd done the day before, and it was only linked to To-Dos. This meant that using skills was pretty detached from how well you were doing in your real life, which defeated the purpose of the game!
p Now you can gain Mana from Dailies and positive Habits as well as To-Dos! You still gain Mana overnight, but how much you gain depends on how many of your Dailies you did.
p You can also lose Mana by hitting negative Habits. Yikes! Better not indulge...
tr
td
h5 SKILLS STRENGTHENED AND WEAKENED
p Some notoriously overpowered skills have been weakened, like Backstab (which often granted over 1000GP per cast), Burst of Flames (which allowed you to 1-hit KO most bosses regardless of how much or how little you'd done that day), and Valorous Presence (which caused the notorious "I checked off one Daily and gained 997 levels" bug reports).
p Some painfully UNDERpowered skills have been strengthened, such as Tools of the Trade and Searing Brightness. We think you'll like these much better now!
p For a full description of what changed, <a href='http://habitrpg.wikia.com/wiki/User_blog:LadyAlys/Skills_Rebalancing_and_Other_Changes' target='_blank'>check out this blog post</a>.
tr
td
h5 GIVING FEEDBACK
p We understand that these changes may be disruptive to some of you. We're sorry! We hope that with time, you will find them to be a drastic improvement. We will be waiting two weeks (so that everyone can get used to the new system), and then on May 15th will open up a new Trello card for comments and feedback.
p For those of you who want to reallocate stats now that you have the new system, please post your User ID in this <a href='https://github.com/HabitRPG/habitrpg/issues/5082' target='_blank'>Github ticket</a>. (Your UUID is found under Settings > API; do NOT post your API Token.) That ticket is also being used to debate a possibility to make it so that reallocating stats does not cost gems. If that's a feature you would like (or would hate), definitely drop by that ticket and let us know!
p.small.muted by Alys, Verabird, brandonjreid, ShilohT, betaveros, SabreCat, chimericdream, and everyone who commented with feedback on Trello and Github. You all are great!
h5 4/30/2015 - LAST CHANCE FOR BUSY BEE AND SPRING FLING ITEMS, AND SOON-TO-APPEAR NEW EQUIPMENT! h5 4/30/2015 - LAST CHANCE FOR BUSY BEE AND SPRING FLING ITEMS, AND SOON-TO-APPEAR NEW EQUIPMENT!
hr hr
tr tr

View file

@ -8,16 +8,52 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
.task-column(class='{{list.type}}s') .task-column(class='{{list.type}}s')
// Todos export/graph options // Todos export/graph options
span.option-box.pull-right(ng-if='::main && list.type=="todo"') span.option-box.pull-right(ng-if='::main')
a.option-action(ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip=env.t('progress')) a.option-action(ng-if='list.type=="todo"', ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip=env.t('progress'), style='margin-right:5px;')
span.glyphicon.glyphicon-signal span.glyphicon.glyphicon-signal
//a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal') //a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal')
a.option-action(ng-click='notPorted()', tooltip='iCal', ng-show='false') //-a.option-action(ng-if='list.type=="todo"', ng-click='notPorted()', tooltip='iCal', ng-show='false')
span.glyphicon.glyphicon-calendar span.glyphicon.glyphicon-calendar
// <a href="https://www.google.com/calendar/render?cid={{encodeiCalLink(_user.id, _user.apiToken)}}" rel=tooltip title="Google Calendar"><i class=icon-calendar></i></a> // <a href="https://www.google.com/calendar/render?cid={{encodeiCalLink(_user.id, _user.apiToken)}}" rel=tooltip title="Google Calendar"><i class=icon-calendar></i></a>
a.option-action(ng-click='list.help=!list.help', tooltip='Click for help')
span.glyphicon.glyphicon-question-sign
// Header // Header
h2.task-column_title {{list.header}} h2.task-column_title
| {{list.header}}
div(ng-if='list.help', ng-switch='::list.type')
div(ng-switch-when='habit')
ul
li Good Habits are things that you do often. They award gold and experience every time you click the <code>+</code>.
li Bad Habits are things you want to avoid doing. They remove health every time you click the <code>-</code>.
li For inspiration, check out these <a href='http://habitrpg.wikia.com/wiki/Sample_Habits#Business' target='_blank'>sample Habits</a>
li More questions? Ask in <a href='https://habitrpg.com/#/options/groups/guilds/5481ccf3-5d2d-48a9-a871-70a7380cee5a' target='_blank'>the Newbies Guild!</a>
div(ng-switch-when='daily')
ul
li Dailies repeat <strong>every day</strong> that they are active. Click the pencil icon to change the days a Daily is active.
li If you dont complete active Dailies, they will hurt your avatar when your day rolls over.
li Dailies turn <strong>redder</strong> when you miss them, and <strong>bluer</strong> when you complete them. The redder the Daily, the more it will reward you... and hurt you.
li To change when your day rolls over, go to Settings > Site > Custom Day Start.
li For inspiration, check out these <a href='http://habitrpg.wikia.com/wiki/Sample_Dailies#Business' target='_blank'>sample Dailies</a>!
li More questions? Ask in <a href='https://habitrpg.com/#/options/groups/guilds/5481ccf3-5d2d-48a9-a871-70a7380cee5a' target='_blank'>the Newbies Guild!</a>
div(ng-switch-when='todo')
ul
li To-Dos start yellow, and get redder (more valuable) the longer it takes to complete them.
li To-Dos never hurt you! They only award gold and experience.
li Breaking a To-Do down into a small checklist will make it less scary, and will increase your points!
li For inspiration, check out these <a href='http://habitrpg.wikia.com/wiki/Sample_To-Dos#Finances' target='_blank'>sample To-Dos</a>!
li More questions? Ask in <a href='https://habitrpg.com/#/options/groups/guilds/5481ccf3-5d2d-48a9-a871-70a7380cee5a' target='_blank'>the Newbies Guild!</a>
div(ng-switch-when='reward')
ul
li The Equipment you buy for your avatar is stored in Inventory > Equipment.
li Equipment affects your stats (Avatar > Stats).
li There will be special equipment here during World Events.
li Don't be afraid to set custom rewards! Check out some samples <a href='http://habitrpg.wikia.com/wiki/Sample_Custom_Rewards' target='_blank'>here</a>
li More questions? Ask in <a href='https://habitrpg.com/#/options/groups/guilds/5481ccf3-5d2d-48a9-a871-70a7380cee5a' target='_blank'>the Newbies Guild!</a>
// Todo Chart // Todo Chart
.todos-chart(ng-if='::list.type == "todo"', ng-show='charts.todos') .todos-chart(ng-if='::list.type == "todo"', ng-show='charts.todos')

View file

@ -1,4 +1,4 @@
li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"] | conditionalOrderBy: list.view=="dated":"date"', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', ng-show='shouldShow(task, list, user.preferences)') li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"] | conditionalOrderBy: list.view=="dated":"date"', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', popover-trigger='mouseenter', data-popover-html="{{task.popoverOpen ? '' : task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', ng-show='shouldShow(task, list, user.preferences)')
// right-hand side control buttons // right-hand side control buttons
.task-meta-controls .task-meta-controls
@ -53,7 +53,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
span.glyphicon.glyphicon-signal span.glyphicon.glyphicon-signal
| &nbsp; | &nbsp;
// notes // notes
span.task-notes(ng-show='task.notes && !task._editing') span.task-notes(ng-show='task.notes && !task._editing', ng-click='task.popoverOpen = !task.popoverOpen', popover-trigger='click', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}')
span.glyphicon.glyphicon-comment span.glyphicon.glyphicon-comment
| &nbsp; | &nbsp;

View file

@ -69,4 +69,4 @@ script(id='modals/login.html', type='text/ng-template')
a(href='/static/privacy' target='_blank')=env.t('privacy') a(href='/static/privacy' target='_blank')=env.t('privacy')
| . | .
.form-group .form-group
input.btn.btn-primary(type='submit', ng-disabled='registrationForm.$invalid', value=env.t('register')) input.btn.btn-primary(type='submit', ng-disabled='registrationForm.$invalid || registrationInProgress', value=env.t('register'))