Added support in daily tasks for specifying ’every X days’ and ‘every X weeks on specific days of the week’.

- shouldDo() now takes the entire task as an input instead of just the ‘repeat’ (days of the week) dictionary.
- the ‘start’ date for a task can be specified (and can also be in the future, in which case it will be greyed out until then.) ‘start’ date also affects ‘every X days’ and ‘every X weeks’.
- no migration code yet.
This commit is contained in:
Allen Pan
2015-05-07 15:23:54 -07:00
parent 2ba1a1fc85
commit 1ecf608408
5 changed files with 122 additions and 24 deletions

View File

@@ -72,13 +72,51 @@ api.daysSince = (yesterday, options = {}) ->
Math.abs api.startOfDay(_.defaults {now:yesterday}, o).diff(api.startOfDay(_.defaults {now:o.now}, o), 'days') Math.abs api.startOfDay(_.defaults {now:yesterday}, o).diff(api.startOfDay(_.defaults {now:o.now}, o), 'days')
### ###
Should the user do this taks on this date, given the task's repeat options and user.preferences.dayStart? Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart?
### ###
api.shouldDo = (day, repeat, options={}) -> api.shouldDo = (day, dailyTask, options = {}) ->
return false unless repeat return false unless dailyTask.type == 'daily' && dailyTask.repeat
# MIGRATION HACK, FIXME
if dailyTask.startDate == null
console.log("null!!!!!")
dailyTask.startDate = moment().toDate();
if dailyTask.startDate instanceof String
console.log("startDate is a string: " + dailyTask.startDate + ". Converting to date");
dailyTask.startDate = moment(dailyTask.startDate).toDate();
o = sanitizeOptions options o = sanitizeOptions options
selected = repeat[api.dayMapping[api.startOfDay(_.defaults {now:day}, o).day()]] dayOfWeek = api.startOfDay(_.defaults {now:day}, o).day()
return selected
# check if event is in the future
hasStartedCheck = moment(day).isAfter(dailyTask.startDate) || moment(day).isSame(dailyTask.startDate)
if dailyTask.frequency == 'daily'
daysSinceTaskStart = api.numDaysApart(day, dailyTask.startDate, o)
everyXCheck = (daysSinceTaskStart % dailyTask.everyX == 0)
return everyXCheck && hasStartedCheck
else if dailyTask.frequency == 'weekly'
dayOfWeekCheck = dailyTask.repeat[api.dayMapping[dayOfWeek]];
weeksSinceTaskStartWeek = api.numWeeksApart(day, dailyTask.startDate, o)
everyXCheck = (weeksSinceTaskStartWeek % dailyTask.everyX == 0)
return dayOfWeekCheck && everyXCheck && hasStartedCheck
else
# unexpected frequency string
return true
api.numDaysApart = (day1, day2, o) ->
startOfDay1 = api.startOfDay(_.defaults {now:day1}, o)
startOfDay2 = api.startOfDay(_.defaults {now:day2}, o)
numDays = Math.abs(startOfDay1.diff(startOfDay2, 'days'))
return numDays
# weeks between the two days, counting Monday as the start of each week
api.numWeeksApart = (day1, day2, o) ->
startOfDay1 = api.startOfDay(_.defaults {now:day1}, o)
startOfDay2 = api.startOfDay(_.defaults {now:day2}, o)
startWeekOfDay1 = startOfDay1.startOf('week')
startWeekOfDay2 = startOfDay2.startOf('week')
numWeeks = Math.abs(startWeekOfDay1.diff(startWeekOfDay2, 'weeks'))
return numWeeks
### ###
------------------------------------------------------ ------------------------------------------------------
@@ -280,7 +318,7 @@ api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, showComplet
# show as completed if completed (naturally) or not required for today # show as completed if completed (naturally) or not required for today
if type in ['todo', 'daily'] if type in ['todo', 'daily']
if completed or (type is 'daily' and !api.shouldDo(+new Date, task.repeat, {dayStart})) if completed or (type is 'daily' and !api.shouldDo(+new Date, task, {dayStart}))
classes += " completed" classes += " completed"
else else
classes += " uncompleted" classes += " uncompleted"
@@ -1537,7 +1575,7 @@ api.wrap = (user, main=true) ->
scheduleMisses = 0 scheduleMisses = 0
_.times daysMissed, (n) -> _.times daysMissed, (n) ->
thatDay = moment(now).subtract({days: n + 1}) thatDay = moment(now).subtract({days: n + 1})
if api.shouldDo(thatDay, repeat, user.preferences) if api.shouldDo(thatDay, task, user.preferences)
scheduleMisses++ scheduleMisses++
if user.stats.buffs.stealth if user.stats.buffs.stealth
user.stats.buffs.stealth-- user.stats.buffs.stealth--

View File

@@ -4,6 +4,9 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Shared, Guide) { function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Shared, Guide) {
$scope.obj = User.user; // used for task-lists $scope.obj = User.user; // used for task-lists
$scope.user = User.user; $scope.user = User.user;
// HACK: flagDict is for storing whether the datePicker popup is open or not. This should just be a boolean flag,
// but apparently due to angular bug need to put the bool in intermediate dict...
$scope.flagDict = {};
$scope.score = function(task, direction) { $scope.score = function(task, direction) {
switch (task.type) { switch (task.type) {
@@ -127,6 +130,24 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
*/ */
$scope._today = moment().add({days: 1}); $scope._today = moment().add({days: 1});
/*
------------------------
Dailies
------------------------
*/
$scope.updateTaskStartDate = function(task) {
// TODO: Weirdness here...startDate should be a Date and _dateString
// should be a string, but the datePicker input fields sets _dateString
// to a Date even though input type="text"...
task.startDate = task._dateString;
};
$scope.open = function($event) {
$event.stopPropagation();
$scope.flagDict['opened'] = !$scope.flagDict['opened'];
}
/* /*
------------------------ ------------------------
Checklists Checklists
@@ -212,7 +233,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
$scope.shouldShow = function(task, list, prefs){ $scope.shouldShow = function(task, list, prefs){
if (task._editing) // never hide a task while being edited if (task._editing) // never hide a task while being edited
return true; return true;
var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task.repeat, prefs) : true; var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true;
switch (list.view) { switch (list.view) {
case "yellowred": // Habits case "yellowred": // Habits
return task.value < 1; return task.value < 1;

View File

@@ -45,6 +45,7 @@ habitrpg
task._editing = !task._editing; task._editing = !task._editing;
task._tags = User.user.preferences.tagsCollapsed; task._tags = User.user.preferences.tagsCollapsed;
task._advanced = User.user.preferences.advancedCollapsed; task._advanced = User.user.preferences.advancedCollapsed;
task._dateString = moment(task.startDate).format('YYYY-MM-DD');
if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false;
}; };
}], }],

View File

@@ -50,10 +50,14 @@ var checklist = [{
var DailySchema = new Schema( var DailySchema = new Schema(
_.defaults({ _.defaults({
type: {type:String, 'default': 'daily'}, type: {type: String, 'default': 'daily'},
//TODO: Cleaner to store interval as enum instead of str?
frequency: {type: String, 'default': 'daily'}, // 'daily', 'weekly'
everyX: {type: Number, 'default': 1}, // e.g. once every X weeks
startDate: {type: Date},
history: Array, history: Array,
completed: {type: Boolean, 'default': false}, completed: {type: Boolean, 'default': false},
repeat: { repeat: { // used only for 'weekly' frequency, TODO: Rename to daysOfWeek or something with 'week'
m: {type: Boolean, 'default': true}, m: {type: Boolean, 'default': true},
t: {type: Boolean, 'default': true}, t: {type: Boolean, 'default': true},
w: {type: Boolean, 'default': true}, w: {type: Boolean, 'default': true},

View File

@@ -166,6 +166,32 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
// if Daily, calendar // if Daily, calendar
fieldset(ng-if='::task.type=="daily"', class="option-group") fieldset(ng-if='::task.type=="daily"', class="option-group")
legend.option-title=env.t('repeat') legend.option-title=env.t('repeat')
// Choose the frequency
ul.priority-multiplier
li
button(ng-class='{active: task.frequency=="daily"}', type='button', ng-click='task.frequency="daily"')='Daily'
li
button(ng-class='{active: task.frequency=="weekly"}', type='button', ng-click='task.frequency="weekly"')='Weekly'
// If frequency is daily
fieldset(ng-show='task.frequency=="daily"')
| Once every&nbsp;&nbsp;
select(ng-model='task.everyX')
option(ng-repeat='x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') {{x}}
| days<br><br>
| Start date:&nbsp;
p(class='input-group')
input(type='text' class='form-control' datepicker-popup ng-model='task._dateString' ng-change='updateTaskStartDate(task)' is-open='flagDict.opened')
span(class='input-group-btn')
button(type="button" ng-click="open($event)")
span.input-group-addon.glyphicon.glyphicon-calendar
// If frequency is weekly
fieldset(ng-show='task.frequency=="weekly"')
| Once every&nbsp;
select(ng-model='task.everyX')
option(ng-repeat='x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') {{x}}
| weeks on these days: <br><br>
ul.repeat-days(bindonce) ul.repeat-days(bindonce)
// note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding
li li
@@ -182,6 +208,14 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}} button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}}
li li
button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}} button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}}
| <br><br>
| Start date:&nbsp;
p(class='input-group')
input(type='text' class='form-control' datepicker-popup ng-model='task._dateString' ng-change='updateTaskStartDate(task)' is-open='flagDict.opened')
span(class='input-group-btn')
button(type="button" ng-click="open($event)")
span.input-group-addon.glyphicon.glyphicon-calendar
// if Reward, pricing // if Reward, pricing
fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id') fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id')