adult/adult.py

708 lines
25 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" How do I even adult? """
import math
import collections
import datetime
import calendar
import click
import flask
import poobrains
app = poobrains.app
def tomorrow():
return datetime.datetime.now() + datetime.timedelta(days=1)
def firstweekday(weekday, first_day_of_month):
""" return day of month which is first occurence of weekday in a month beginning on weekday first_day_of_month. """
weekday = weekday - 1
first_day_of_month = first_day_of_month - 1
if first_day_of_month <= weekday:
return weekday - first_day_of_month + 1
elif first_day_of_month > weekday:
return weekday - first_day_of_month + 7 + 1
class TaskForm(poobrains.form.AutoForm):
def __init__(self, model_or_instance, **kwargs):
super(TaskForm, self).__init__(model_or_instance, **kwargs)
choices = []
for task in Task.list('read', flask.g.user):
choices.append((task, task.title))
dep_value = [x.dependency for x in self.instance.task_dependencies]
self.dependencies = poobrains.form.fields.Select(type=poobrains.form.types.StorableInstanceParamType(Task), multi=True, choices=choices, value=dep_value)
def process(self, submit):
r = super(TaskForm, self).process(submit)
if submit == 'submit':
dependencies = self.fields['dependencies'].value
TaskDependency.delete().where(TaskDependency.task == self.instance).execute()
try:
for dependency in dependencies:
newdep = TaskDependency()
newdep.task = self.instance
newdep.dependency = dependency
newdep.save(force_insert=True)
except poobrains.storage.IntegrityError:
poobrains.flash("At least have the decency to build an indirect loop!")
return r
@app.expose('/task', force_secure=True)
class Task(poobrains.commenting.Commentable):
class Meta:
order_by = ('-priority', 'checkdate', '-date')
form_add = TaskForm
form_edit = TaskForm
title = poobrains.storage.fields.CharField()
checkdate = poobrains.storage.fields.DateTimeField(default=tomorrow, null=True)
priority = poobrains.storage.fields.IntegerField(default=0, form_widget=poobrains.form.fields.Select, choices=[
(-2, 'Very low'),
(-1, 'Low'),
(0, 'Normal'),
(1, 'High'),
(2, 'VERY HIGH')
])
status = poobrains.storage.fields.CharField(default='new', form_widget=poobrains.form.fields.Select, choices=[
('new', 'new'),
('ongoing', 'ongoing'),
('finished', 'finished'),
('aborted', 'aborted')
])
progress = poobrains.storage.fields.IntegerField(default=0, constraints=[
poobrains.storage.fields.Check('progress >= 0'),
poobrains.storage.fields.Check('progress <= 100')
])
description = poobrains.md.MarkdownField()
reward_served = poobrains.storage.fields.BooleanField(default=False)
@property
def css_class(self):
classes = []
classes.append(self.checkdate_css)
classes.append(self.priority_css)
return ' '.join(classes)
@property
def checkdate_css(self):
if isinstance(self.checkdate, datetime.datetime):
now = datetime.datetime.now()
if self.checkdate < now:
return 'checkdate-passed'
elif self.checkdate - datetime.timedelta(days=1) < now: # checkdate within the next 24h
return 'checkdate-24h'
return ''
@property
def priority_label(self):
""" gives the choice of the currently set priority """
return dict(self.__class__.priority.choices)[self.priority]
@property
def priority_css(self):
return 'priority-%s' % self.priority_label.lower().replace(' ', '-')
@property
def progress_svg(self):
return Progress(handle=self.progress).render('raw')
def validate(self):
pass # FIXME/TODO: dependency resolution
def save(self, **kwargs):
if self._pk and not self.reward_served and self.status == 'finished': # FIXME: _pk is an ugly hack to determin if we're editing or adding but peewee didn't offer anything better last i checked
reward_token = RewardToken()
reward_token.task = self
reward_token.owner = self.owner
reward_token.group = self.group
reward_token.save(force_insert=True)
self.reward_served = True
return super(Task, self).save(**kwargs)
@classmethod
def class_tree(cls, root=None, current_depth=0):
if current_depth == 0:
tree = poobrains.rendering.Tree(root=poobrains.rendering.RenderString('dependencies'), mode='inline')
else:
tree = poobrains.rendering.Tree(root=root, mode='inline')
if current_depth > 100:
if root:
message = "Possible loop in dependencies of Task:'%s'." % root.name
else:
message = "Possible loop in dependencies of a Task but don't have a root for this tree. Are you fucking with current_depth manually?"
app.logger.error(message)
return tree
deps = TaskDependency.select().where(TaskDependency.task == root)
for dep in deps:
tree.children.append(dep.dependency.tree(current_depth=current_depth+1))
return tree
def tree(self, current_depth=0):
return self.__class__.class_tree(root=self, current_depth=current_depth)
class RecurringTask(poobrains.commenting.Commentable):
title = poobrains.storage.fields.CharField()
checkdate = poobrains.storage.fields.IntegerField(null=True, help_text='Time frame in seconds')
priority = poobrains.storage.fields.IntegerField(form_widget=poobrains.form.fields.Select, choices=[
(-2, 'Very low'),
(-1, 'Low'),
(0, 'Normal'),
(1, 'High'),
(2, 'VERY HIGH')
])
year = poobrains.storage.fields.IntegerField(null=True)
month = poobrains.storage.fields.IntegerField(null=True, form_widget=poobrains.form.fields.Select, choices=[
(None, 'Any'),
(1, 'January'),
(2, 'February'),
(3, 'March'),
(4, 'April'),
(5, 'May'),
(6, 'June'),
(7, 'July'),
(8, 'August'),
(9, 'September'),
(10, 'October'),
(11, 'November'),
(12, 'December')
])
weeks = poobrains.storage.fields.IntegerField(null=True, help_text="Every n weeks after creation.")
weekday_month = poobrains.storage.fields.IntegerField(null=True, form_widget=poobrains.form.fields.Select, choices=[(None, 'Any')] + [(x, x) for x in range(1,7)])
weekday = poobrains.storage.fields.IntegerField(null=True, form_widget=poobrains.form.fields.Select, choices=[
(None, 'Any'),
(1, 'Monday'),
(2, 'Tuesday'),
(3, 'Wednesday'),
(4, 'Thursday'),
(5, 'Friday'),
(6, 'Saturday'),
(7, 'Sunday')
])
day = poobrains.storage.fields.IntegerField(null=True, form_widget=poobrains.form.fields.Select, choices=[(None, 'Any')] + [(x, x) for x in range(1,32)])
hour = poobrains.storage.fields.IntegerField(null=True, choices=[(None, 'Any')] + [(x, x) for x in range(0,24)], help_text='0-23')
minute = poobrains.storage.fields.IntegerField(null=True, choices=[(None, 'Any')] + [(x, x) for x in range(0,60)], help_text='0-59')
description = poobrains.md.MarkdownField()
latest_task = poobrains.storage.fields.ForeignKeyField(Task, null=True)
class TaskDependency(poobrains.storage.Model):
class Meta:
order_by = ['task']
primary_key = poobrains.storage.CompositeKey('task', 'dependency')
task = poobrains.storage.fields.ForeignKeyField(Task, related_name='task_dependencies')
dependency = poobrains.storage.fields.ForeignKeyField(Task, related_name='provides', constraints=[poobrains.storage.fields.Check('dependency_id <> task_id')])
@app.expose('/rewards/', mode='full')
class Reward(poobrains.commenting.Commentable):
title = poobrains.storage.fields.CharField()
description = poobrains.md.MarkdownField(null=True)
class RedeemForm(poobrains.auth.BoundForm):
title = "Redeem a token"
def __init__(self, *args, **kwargs):
super(RedeemForm, self).__init__(*args, **kwargs)
choices = []
for choice in self.instance.reward_choices:
choices.append((choice.reward, choice.reward.render('inline')))
self.reward = poobrains.form.fields.Radio(choices=choices, type=poobrains.form.types.StorableInstanceParamType(Reward))
self.submit = poobrains.form.Button(type='submit', label=u'🍰')
def process(self, *args, **kwargs):
reward = self.fields['reward'].value
if reward:
self.instance.reward = reward
self.instance.redeemed = True
self.instance.save()
poobrains.flash("Token redeemed, enjoy your reward now! :)")
return poobrains.redirect(reward.url('full'))
flash(u"The cake might be a lie. 🤔", 'error')
return self
@app.expose('/tokens/', mode='redeem')
class RewardToken(poobrains.auth.Administerable):
task = poobrains.storage.fields.ForeignKeyField(Task)
reward = poobrains.storage.fields.ForeignKeyField(Reward, null=True)
redeemed = poobrains.storage.fields.BooleanField(default=False)
form_redeem = RedeemForm # tells self.form to use RedeemForm for mode 'redeem' updates
class Meta:
modes = collections.OrderedDict([
('add', 'create'),
('teaser', 'read'),
('inline', 'read'),
('full', 'read'),
('edit', 'update'),
('delete', 'delete'),
('redeem', 'update')
])
def save(self, **kwargs):
rv = super(RewardToken, self).save(**kwargs)
if not len(self.reward_choices):
for reward in Reward.select().order_by(poobrains.storage.fn.Random()).limit(5):
choice = RewardTokenChoice()
choice.token = self
choice.reward = reward
choice.save(force_insert=True)
return rv
def redeem(self, reward):
self.reward = reward
self.save()
class RewardTokenChoice(poobrains.storage.Model):
class Meta:
primary_key = poobrains.storage.CompositeKey('token', 'reward')
order_by = ['token', 'reward']
token = poobrains.storage.fields.ForeignKeyField(RewardToken, related_name='reward_choices')
reward = poobrains.storage.fields.ForeignKeyField(Reward)
class TaskControl(poobrains.auth.Protected):
user = None
new = None
ongoing = None
finished = None
aborted = None
def __init__(self, handle=None, **kwargs):
super(TaskControl, self).__init__(**kwargs)
self.user = poobrains.auth.User.load(handle)
self.menu_actions = poobrains.rendering.Menu('task-add')
self.menu_actions.append(Task.url('add'), 'Add new')
base_query = Task.list('read', poobrains.g.user).where(Task.owner == self.user)
self.new = base_query.where(Task.status == 'new')
self.ongoing = base_query.where(Task.status == 'ongoing')
self.finished = base_query.where(Task.status == 'finished', Task.checkdate > datetime.datetime.now())
self.aborted = base_query.where(Task.status == 'aborted')
@property
def title(self):
return "%ss goals" % self.user.name
# @property
# def menu_actions(self):
# m = poobrains.rendering.Menu('actions')
# m.append('#', poobrains.Markup('<span class="priority priority-very-low">vewy low</span>'))
#
# return m
# def view(self, handle=None, offset=0, **kwargs):
#
# super(TaskControl, self).view(handle=handle, **kwargs) # checks permissions
# u = poobrains.auth.User.load(handle)
#
# q = Task.list('read', poobrains.g.user).where(Task.owner == u, Task.status != 'aborted', Task.status != 'finished').order_by(Task.priority.desc(), Task.checkdate.desc(), Task.created, Task.title)
#
# listing = poobrains.storage.Listing(Task, title="Your goals", query=q, offset=offset, pagination_options={'handle': handle})
#
# return listing.view(**kwargs)
app.site.add_view(TaskControl, '/~<handle>/tasks/', mode='full', endpoint='taskcontrol_handle')
app.site.add_view(TaskControl, '/~<handle>/tasks/+<int:offset>', mode='full', endpoint='taskcontrol_handle_offset')
@app.expose('/svg/progress')
class Progress(poobrains.svg.SVG):
width = '100%'
height = '20px'
@property
def percent(self):
return int(self.handle)
def view(self, mode=None, handle=None, **kwargs):
try:
self.percent # makes sure handle makes sense
return super(Progress, self).view(mode=mode, handle=handle, **kwargs)
except TypeError:
abort(400, 'Progress value must be 0-100.')
@app.route('/')
def front():
return poobrains.redirect(TaskControl.url(handle=poobrains.g.user.name, mode='full'))
@app.cron
def create_recurring():
now = datetime.datetime.now()
dated_tasks = collections.OrderedDict()
for template in RecurringTask.select():
try:
if template.latest_task:
base_date = template.latest_task.date
else: # for some reason this can be None without triggering Task.DoesNotExist
base_date = template.date
except Task.DoesNotExist:
base_date = template.date
dates = collections.OrderedDict()
# add years
for year in range(base_date.year, now.year + 1):
if not template.year or template.year == year:
dates[year] = collections.OrderedDict()
years = list(dates.keys())
# add months
for year, _ in dates.items():
# Determine whether this is the first, last (or neither) year of the range
first_year = year == years[0]
last_year = year == years[-1]
if not template.month is None:
if first_year and last_year:
valid = template.month >= base_date.month and template.month <= now.month
elif first_year:
valid = template.month >= base_date.month
elif last_year:
valid = template.month <= now.month
else:
valid = True
if valid:
dates[year][template.month] = collections.OrderedDict()
else:
if first_year and last_year:
months = range(base_date.month, now.month + 1)
elif first_year:
months = range(base_date.month, 13)
elif last_year:
months = range(1, now.month + 1)
else:
months = range(1, 13)
for month in months:
dates[year][month] = collections.OrderedDict()
# add days
for year, months in dates.items():
first_year = year == years[0]
last_year = year == years[-1]
for month, _ in months.items():
first_month = first_year and month == base_date.month
last_month = last_year and month == now.month
first_day_of_month = datetime.datetime(year=year, month=month, day=1)
if not template.day is None:
#first_year_valid = first_year and month <= base_date.month
#middle_year_valid = not first_year and not last_year
#last_year_valid = last_year and month >= now.month
#weekday_valid = not template.weekday or datetime.datetime(year, month, template.day).isoweekday == template.weekday - 1
if first_month and last_month:
valid = template.day >= base_date.day and template.day <= now.day
elif first_month:
valid = template.day >= base_date.day
elif last_month:
valid = template.day <= now.day
else:
valid = True
valid = valid and (not template.weekday or datetime.datetime(year, month, template.day).isoweekday() == template.weekday) # check if the date has the correct weekday
valid = valid and (not template.weekday_month or day == firstweekday(template.weekday, first_day_of_month.isoweekday()) + (7 * (template.weekday_month -1))) # check if date has correct'th number of weekday occurence in this month (2nd friday or whatev)
week_valid = False
if template.weeks:
base_monday = datetime.datetime(year=base_date.year, month=base_date.month, day=base_date.day - base_date.weekday())
current_monday = dt - datetime.timedelta(days=dt.weekday())
delta = current_monday - base_monday
if delta.days % (7 * template.weeks) == 0:
week_valid = True
else:
week_valid = True
#if (first_year_valid or middle_year_valid or last_year_valid) and weekday_valid:
if valid and week_valid:
dates[year][month][template.day] = collections.OrderedDict()
else:
if first_month and last_month:
days = range(base_date.day, now.day + 1)
elif first_month:
days = range(base_date.day, calendar.monthrange(year, month)[1] + 1)
elif last_month:
days = range(1, now.day + 1)
else:
days = range(1, 32) # days that don't exist are weeded out by trying to create a datetime from it
for day in days:
try:
dt = datetime.datetime(year=year, month=month, day=day)
except ValueError:
continue # means we have an invalid date on our hands, skip to next iteration of the loop
#weekday_distance = template.weekday - first_day_of_month.isoweekday()
week_valid = False
if template.weeks:
base_monday = datetime.datetime(year=base_date.year, month=base_date.month, day=base_date.day - base_date.weekday())
current_monday = dt - datetime.timedelta(days=dt.weekday())
delta = current_monday - base_monday
if delta.days % (7 * template.weeks) == 0:
week_valid = True
else:
week_valid = True
weekday_valid = not template.weekday or dt.isoweekday() == template.weekday
if week_valid and weekday_valid:
#weekday_month_valid = not template.weekday_month or (day + weekday_distance) / 7.0 == template.weekday_month - 1
weekday_month_valid = not template.weekday_month or day == firstweekday(template.weekday, first_day_of_month.isoweekday()) + (7 * (template.weekday_month -1))
if weekday_month_valid:
dates[year][month][day] = collections.OrderedDict()
# add hours
for year, months in dates.items():
first_year = year == years[0]
last_year = year == years[-1]
for month, days in months.items():
first_month = first_year and month == base_date.month
last_month = last_year and month == now.month
for day, _ in days.items():
first_day = first_month and day == base_date.day
last_day = last_month and day == now.day
if not template.hour is None:
if first_day and last_day:
valid = template.hour >= base_date.hour and template.hour <= now.hour
elif first_day:
valid = template.hour >= base_date.hour
elif last_day:
valid = template.hour <= now.hour
else:
valid = True
if valid:
dates[year][month][day][template.hour] = collections.OrderedDict()
else:
if first_day and last_day:
hours = range(base_date.hour, now.hour + 1)
elif first_day:
hours = range(base_date.hour, 24)
elif last_day:
hours = range(0, now.hour + 1)
else:
hours = range(0,24)
for hour in hours:
dates[year][month][day][hour] = collections.OrderedDict()
task_dates = []
# add minutes
for year, months in dates.items():
first_year = year == years[0]
last_year = year == years[-1]
for month, days in months.items():
first_month = first_year and month == base_date.month
last_month = last_year and month == now.month
for day, hours in days.items():
first_day = first_month and day == base_date.day
last_day = last_month and day == now.day
for hour, _ in hours.items():
first_hour = first_day and hour == base_date.hour
last_hour = last_day and hour == now.hour
if not template.minute is None:
if first_hour and last_hour:
valid = template.minute > base_date.minute and template.minute <= now.minute
elif first_hour:
valid = template.minute > base_date.minute
elif last_hour:
valid = template.minute <= now.minute
else:
valid = True
if valid:
task_dates.append(datetime.datetime(year=year, month=month, day=day, hour=hour, minute=template.minute))
else:
if first_hour and last_hour:
for minute in range(base_date.minute + 1, now.minute + 1):
task_dates.append(datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute))
elif first_hour:
for minute in range(base_date.minute + 1, 60):
task_dates.append(datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute))
elif last_hour:
for minute in range(0, now.minute + 1):
task_dates.append(datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute))
else:
for minute in range(0, 60):
task_dates.append(datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute))
click.echo("Creating %d tasks for '%s'." % (len(task_dates), template.title))
with click.progressbar(task_dates) as proxy:
for date in proxy:
task = Task()
task.name = "%s-%d-%d-%d-%d-%d" % (template.name, date.year, date.month, date.day, date.hour, date.minute)
task.owner = template.owner
task.group = template.group
task.status = 'new'
task.title = template.title
task.date = date
if template.checkdate:
task.checkdate = date + datetime.timedelta(seconds=template.checkdate)
task.priority = template.priority
task.description = template.description
task.save(force_insert=True)
for tag in template.tags:
binding = poobrains.tagging.TagBinding()
binding.priority = 42
binding.tag = tag
binding.model = Task.__name__
binding.handle = task.handle_string
binding.save()
if len(task_dates):
template.latest_task = task
template.save()
if __name__ == '__main__':
app.cli()