def _workspace(): action = request.form.get("action") if action == "create": w = Workspace(sanitize_workspacename(request.form.get("name")), request.user) c = Context(unicode(_("Unsorted"))) w.contexts.append(c) db.session.add(w) try: db.session.commit() except IntegrityError: return jsonify({"message": unicode(_("Workspace '%s' already exists!", w.name))}) return jsonify({"url": url_for('index_workspace', workspace=w.name)}) return jsonify({})
def preferences(): edittitle = unicode(_("Preferences")) if not request.form: form = PreferencesForm(obj=request.user) return jsonify({"title": edittitle, "body": render_template("edit.html", form=form, type="prefs")}) form = PreferencesForm(request.form, obj=request.user) if form.validate(): form.populate_obj(request.user) if form.pwd1.data: request.user.set_password(form.pwd1.data) have_workspace = not not getattr(request, "workspace") if have_workspace: flash(_("Saved preferences")) db.session.commit() return jsonify({"status": "LOCALECHANGE" if request.changed_locale else ("" if have_workspace else "NOWORKSPACE")}) return jsonify({"status": "AGAIN", "title": edittitle, "body": render_template("edit.html", form=form, type="prefs")})
def fire(self, is_first_run): if is_first_run: return from dtg.model import User from dtg.transtools import _ today = date.today() tomorrow = today + timedelta(days=1) for user in User.query.filter_by(do_notify=True).all(): if not user.email: continue request.user = user for workspace in user.workspaces: contexts = [] request.workspace = workspace for context in workspace.contexts: tasks = [] for task in context.tasks: if not task.completed and not (task.recur_procedure or task.recur_data) and task.notify and task.due is not None and task.due < tomorrow \ and (task.visible_from is None or task.visible_from < tomorrow): tasks.append({"name": task.summary, "id": task.id, "due_marker": task.due_marker, "is_recurring": task.master_task is not None, "body": markdown.markdown(task.description, safe_mode="escape"), "tags": ", ".join(tag.name for tag in task.tags)}) if tasks: contexts.append({"name": context.name, "tasks": tasks}) if contexts: mail_body = render_template("mail.html", contexts=contexts, workspace_name=workspace.name) send_html_message(u"⌚ DTG – " + workspace.name + _(u" – Upcoming tasks"), "DTG <" + user.email + ">", user.email, mail_body, [], "localhost")
def is_recur_proc(form, field): from dtg.model import Task t = Task("", "") t.recur_procedure = field.data try: t.rrule except RecurInfoException, e: raise ValidationError(_(e.args[0][0], e.args[0][1]))
def login(): if session.get("username") is not None: flash(_("You are already logged in!")) db.session.commit() return redirect(url_for("index")) if request.method == 'POST': username = request.form['username'] password = request.form["password"] user = User.query.filter_by(username=username).first() if not user or user.password != get_password_hash(user.salt, password): return render_error(_("Invalid credentials.")) session['username'] = username if request.args.get('url'): return redirect(request.args['url'].encode("ascii")) else: return redirect(url_for('index')) return render_template("login.html")
def reschedule(self, flash=True): from dtg.webapp import flash next = self.compute_next_date() assert next, "Can only schedule master tasks" self.recur_last = next self.create_slave_task(next) if flash: flash(_("Rescheduled task"))
def is_recur_info(form, field): from dtg.model import Task t = Task("", "") t.recur_data = field.data try: t.rrule except RecurInfoException, e: raise ValidationError(_(e.message[0], e.message[1]))
def workspace_rename(workspace): name = request.form.get("name") name = sanitize_workspacename(name) workspace.name = name try: db.session.commit() except IntegrityError: return jsonify({"message": unicode(_("Workspace '%s' already exists!", name))}) return jsonify({"name": name})
def workspace_tags(workspace): action = request.form.get("action") if not action: return jsonify({"data": [{"name": tag.name, "id": tag.id} for tag in workspace.tags], "seqid": workspace.seqid}) else: tag = Tag.query.filter_by(id=int(request.form.get("id")), workspace=workspace).first() edittitle = _("Edit tag") if tag else _("Add tag") if action == "move": previous_tag = Tag.query.filter_by(id=int(request.form.get("previous_id")), workspace=workspace).first() old_index = workspace.tags.index(tag) if previous_tag is None: new_index = 0 else: new_index = workspace.tags.index(previous_tag) + 1 if old_index < new_index: new_index -= 1 workspace.tags.pop(old_index) workspace.tags.insert(new_index, tag) elif action == "editinitial": form = TagForm(obj=tag) return jsonify({"title": edittitle, "body": render_template("edit.html", form=form, type="tag")}) elif action == "edit": form = TagForm(request.form, obj=tag) try: if form.validate(): if tag is None: tag = Tag(form["name"].data) workspace.tags.append(tag) else: form.populate_obj(tag) flash(_("Saved tag '%s'", tag.name)) db.session.commit() return jsonify({"id": tag.id}) except IntegrityError: form["name"].errors.append(_("Duplicate name")) return jsonify({"status": "AGAIN", "title": edittitle, "body": render_template("edit.html", form=form, type="tag")}) elif action == "delete": flash(_("Deleted tag '%s'", tag.name)) tag.delete() else: return jsonify({"error": "Invalid action"}) db.session.commit() return jsonify({"result": "OK"})
def due_marker(self): if not self.due: return days = (self.due - date.today()).days if days < 0: days_text = _("Overdue for %i days", (-days, )) klass = "overdue" elif days == 0: days_text = _("Due today") klass = "duetoday" elif days > 0: if days == 1: days_text = _("Due tomorrow") else: days_text = _("Due in %i days", (days, )) if days > 7: klass = "duefuture" else: klass = "duesoon" return days, unicode(days_text), klass
def get_filtered_tasks(workspace, tasks, form): ignore_tag_flt = False if form.get("tagexcl") is None: ignore_tag_flt = True timefilter = "fltall" kindfilter = "flttodo" else: tagexcl = bool(int(form.get("tagexcl"))) timefilter = form.get("timefilter") kindfilter = form.get("kindfilter") today = date.today() tomorrow = today + timedelta(days=1) dayaftertomorrow = tomorrow + timedelta(days=1) inaweek = tomorrow + timedelta(days=7) if kindfilter == "flttodo": def fltr(item): return not item.completed and not (item.recur_procedure or item.recur_data) elif kindfilter == "fltdone": def fltr(item): return item.completed elif kindfilter == "fltinvisible": def fltr(item): return not item.completed and not (item.recur_procedure or item.recur_data) elif kindfilter == "flttmpl": def fltr(item): return not item.completed and (item.recur_procedure or item.recur_data) if timefilter == "fltall": def fltr2(item): if kindfilter == "flttmpl": return True return (item.visible_from is not None and item.visible_from > today) if kindfilter == "fltinvisible" else (item.visible_from is None or item.visible_from <= today) elif timefilter == "flttoday": def fltr2(item): return not item.completed and (item.due is None or item.due < tomorrow) and (item.visible_from is None or item.visible_from < tomorrow) elif timefilter == "fltplus1": def fltr2(item): return not item.completed and (item.due is None or item.due < dayaftertomorrow) and (item.visible_from is None or item.visible_from < dayaftertomorrow) elif timefilter == "fltplus7": def fltr2(item): return not item.completed and (item.due is None or item.due < inaweek) and (item.visible_from is None or item.visible_from < inaweek) elif timefilter == "fltnodate": def fltr2(item): return not item.completed and not item.due and (item.visible_from is None or item.visible_from < tomorrow) else: return render_error(_("Invalid filter name."), True) tags = set(Tag.query.filter_by(id=int(key), workspace=workspace).first() for key in form.getlist("selected_tags[]")) if ignore_tag_flt: func = lambda x: True else: func = tags.__ge__ if tagexcl else lambda tasktags: not tags or (tags & tasktags) return [task for task in tasks if fltr(task) and fltr2(task) and func(set(task.tags))]
def workspace_contexts(workspace): action = request.form.get("action") if not action: return jsonify({"data": [{"name": context.name, "id": context.id, "total": len([t for t in context.tasks if not t.completed]), "count": len(get_filtered_tasks(workspace, context.tasks, request.form))} for context in workspace.contexts], "seqid": workspace.seqid}) else: context = Context.query.filter_by(id=int(request.form.get("id")), workspace=workspace).first() edittitle = unicode(_("Edit context") if context else _("Add context")) if action == "move": previous_context = Context.query.filter_by(id=int(request.form.get("previous_id")), workspace=workspace).first() old_index = workspace.contexts.index(context) if previous_context is None: new_index = 0 else: new_index = workspace.contexts.index(previous_context) + 1 if old_index < new_index: new_index -= 1 workspace.contexts.pop(old_index) workspace.contexts.insert(new_index, context) elif action == "move_task": task = Task.query.filter_by(id=int(request.form.get("task_id"))).first() if task.context.workspace != workspace: return render_error(_("Wrong workspace"), True) task.context.tasks.remove(task) context.tasks.insert(0, task) elif action == "editinitial": form = ContextForm(obj=context) return jsonify({"title": edittitle, "body": render_template("edit.html", form=form, type="context")}) elif action == "edit": form = ContextForm(request.form, obj=context) try: if form.validate(): if context is None: context = Context(form["name"].data) workspace.contexts.append(context) else: form.populate_obj(context) flash(_("Saved context '%s'", context.name)) db.session.commit() return jsonify({}) except IntegrityError: form["name"].errors.append(_("Duplicate name")) return jsonify({"status": "AGAIN", "title": edittitle, "body": render_template("edit.html", form=form, type="context")}) elif action == "delete": if len(workspace.contexts) == 1: return render_error(_("Last context cannot be deleted"), True) context.delete() flash(_("Deleted context '%s'", context.name)) else: return jsonify({"error": "Invalid action"}) db.session.commit() return jsonify({"result": "OK"})
def rrule(self): _ = lambda x: x kwargs = {} if self.recur_data: values = self.recur_data.split(";") if values[0] not in ("Y", "M", "W", "D"): raise RecurInfoException( (_("Invalid recurrence type"), {})) for i, value in enumerate(values[1:]): if i <= self.recur_last_arg_field: field_name = self.recur_fields[i] kwargs[field_name] = int(value) else: try: kwargname, value = value.split("=", 1) except ValueError: raise RecurInfoException((_( "Invalid token '%(field)s', expected parameter name" ), { "field": value })) try: value = json.loads(value) except ValueError, e: raise RecurInfoException( (_("Invalid data in field '%(field)s'"), { "field": kwargname })) kwargs[kwargname] = value kwargs.update(self.get_default_rrule_args()) freq = { "Y": YEARLY, "M": MONTHLY, "W": WEEKLY, "D": DAILY }[values[0]] return rrule(freq, **kwargs)
class Task(CreationTimeMixin, db.Model): """ A task can have one of three types: single event, master task, slave task. A master task contains the recurrence pattern (either as a procedure or a data field) and creates many slave tasks as long as it is not completed yet. Deleting and completing a slave task whose master task is not hard-scheduled forces a new slave task to be created immediately. Disabling hard-scheduling for a master task creates a slave task if there is no non-completed one. Creating a non-hard-scheduled master task creates a slave task. """ id = db.Column(db.Integer, primary_key=True) position = db.Column(db.Integer, nullable=False) summary = db.Column(db.String(255), nullable=False) description = db.Column(db.Text(), nullable=False) visible_from = db.Column(db.Date) due = db.Column(db.Date) notify = db.Column(db.Boolean, nullable=False) completed = db.Column(db.Boolean, nullable=False) context_id = db.Column(db.Integer, db.ForeignKey("context.id"), nullable=False) context = db.relationship( "Context", backref=db.backref("tasks", collection_class=ordering_list("position"), order_by=[position])) master_task_id = db.Column(db.Integer, db.ForeignKey("task.id")) master_task = db.relationship("Task", backref=db.backref("slaves", remote_side=[id], uselist=True), remote_side=[master_task_id], uselist=False) tags = db.relationship('Tag', secondary=tasks2tags, backref=db.backref('tasks'), order_by=[Tag.position]) completion_time = db.Column(db.DateTime) recur_data = db.Column(db.String(1024)) recur_procedure = db.Column(db.String(256)) recur_last = db.Column(db.Date) recur_hardschedule = db.Column(db.Boolean) recur_fields = [ "interval", "setpos", "bymonth", "bymonthday", "byyearday", "byweekno", "byweekday", "byeaster" ] recur_last_arg_field = 0 recur_reschedule_this = False def __init__(self, summary, description, context=None, visible_from=None, due=None, notify=True, completed=False, master_task=None, tags=None): self.summary = summary self.description = description if context is not None: # set via context.tasks self.context = context self.visible_from = visible_from self.due = due self.notify = notify self.completed = completed self.master_task = master_task if tags is None: self.tags = [] else: self.tags = tags def create_slave_task(self, newduedate): if self.visible_from is None: visfrom = None else: visfrom = newduedate - (self.due - self.visible_from) task = Task(self.summary, self.description, None, visfrom, newduedate, self.notify, self.completed, self, self.tags) self.context.tasks.insert(0, task) return task def compute_next_date(self): due_set = self.due is not None if (self.recur_last is None and not due_set) or not self.recur_hardschedule: self.recur_last = date.today() elif due_set and self.recur_last is None: return self.due rrule = self.rrule if not rrule: return None next, next2 = rrule[:2] next = next.date() next2 = next2.date() if next == self.recur_last: next = next2 return next def reschedule(self, flash=True): from dtg.webapp import flash next = self.compute_next_date() assert next, "Can only schedule master tasks" self.recur_last = next self.create_slave_task(next) if flash: flash(_("Rescheduled task")) def delete(self): if self.master_task and not self.master_task.recur_hardschedule: self.master_task.reschedule() db.session.delete(self) @property def recur_next(self): if not (self.recur_data or self.recur_procedure): return return self.compute_next_date().isoformat() def get_default_rrule_args(self): return {"dtstart": self.recur_last} @property def due_marker(self): if not self.due: return days = (self.due - date.today()).days if days < 0: days_text = _("Overdue for %i days", (-days, )) klass = "overdue" elif days == 0: days_text = _("Due today") klass = "duetoday" elif days > 0: if days == 1: days_text = _("Due tomorrow") else: days_text = _("Due in %i days", (days, )) if days > 7: klass = "duefuture" else: klass = "duesoon" return days, unicode(days_text), klass @classmethod def generate_recur_data(cls, type, **data): retval = [type] for i, kwargname in enumerate(cls.recur_fields): value = data[kwargname] if i <= self.recur_last_arg_field: if value is None: retval.append("") else: retval.append(str(value)) elif value is not None: retval.append("%s=%s" % (kwargname, value)) return ";".join(retval) @property def rrule(self): _ = lambda x: x kwargs = {} if self.recur_data: values = self.recur_data.split(";") if values[0] not in ("Y", "M", "W", "D"): raise RecurInfoException( (_("Invalid recurrence type"), {})) for i, value in enumerate(values[1:]): if i <= self.recur_last_arg_field: field_name = self.recur_fields[i] kwargs[field_name] = int(value) else: try: kwargname, value = value.split("=", 1) except ValueError: raise RecurInfoException((_( "Invalid token '%(field)s', expected parameter name" ), { "field": value })) try: value = json.loads(value) except ValueError, e: raise RecurInfoException( (_("Invalid data in field '%(field)s'"), { "field": kwargname })) kwargs[kwargname] = value kwargs.update(self.get_default_rrule_args()) freq = { "Y": YEARLY, "M": MONTHLY, "W": WEEKLY, "D": DAILY }[values[0]] return rrule(freq, **kwargs) elif self.recur_procedure: try: freq, args = get_rrule_args(localeEnglish, self.recur_procedure) except ValueError, e: raise RecurInfoException( (_("Invalid recurrence procedure, see examples"), ()))
def __call__(self, form, field): other_field = form[self.fieldname] if field.data and not other_field.data: raise ValidationError(_(self.message))
def __call__(self, form, field): other_field = form[self.smaller_than_fieldname] if field.data is not None and other_field.data is not None and not field.data <= other_field.data: raise ValidationError( _(self.message, {"otherfield": other_field.label}))
class PreferencesForm(PreferencesBaseForm): locale = SelectField(_("Language"), choices=locale_choices) pwd1 = PasswordField( _('New Password'), [EqualTo('pwd2', message=_('Passwords must match'))]) pwd2 = PasswordField(_('Repeat Password'))
def generate_forms(app, db): global _ from dtg.model import Task, Context, Tag, User with app.app_context(): def model_form_type(*args, **kwargs): kwargs['db_session'] = db.session kwargs['converter'] = DTGModelConverter(None) return model_form(*args, **kwargs) TaskFormBase = model_form_type( Task, exclude=to_exclude, field_args={ "visible_from": { "widget": datepicker, "validators": [ SmallerEqualThan( "due", _("Visible from date must be earlier than due date." )) ], "description": _("The task will be invisible until this day."), }, "due": { "widget": datepicker, "description": _("Until when should the task be completed"), }, "recur_hardschedule": { "label": _("Hard scheduling"), "description": _("If ticked, tasks will be rescheduled regardless of the completion. If not ticked, rescheduling happens when a task is completed." ), }, "recur_data": { "label": _("Expert scheduling code"), "description": _("If you are an expert, you may compose a complex code that controls the schedule of this task." ), "validators": [ is_recur_info, ExclusiveWith( "recur_procedure", _("Please enter either the easy or the expert string, but not both." )), IfSetRequiresOtherField( "due", _("Please set a due date before setting the recurrence interval!" )) ], }, "recur_procedure": { "label": _("Recurrence procedure"), "description": _("Here you may use your English words to describe the recurrence pattern. Supported examples include 'Every month', 'Every 3 months', 'Every Monday', 'Every 3 years', 'Every 42 weeks'." ), "validators": [ is_recur_proc, ExclusiveWith( "recur_data", _("Please enter either the easy or the expert string, but not both." )), IfSetRequiresOtherField( "due", _("Please set a due date before setting the recurrence interval!" )) ], }, "notify": { "description": _("Whether reminders should be generated for this task"), }, "description": { "description": _("Detailed information about this task. In Markdown syntax." ), }, "completed": { "description": _("Tick here if the task has been performed."), } }) # get the field names into the gettext extractor _("Description"), _("Visible From"), _("Due"), _("Notify"), _( "Context"), _("Summary") @make_global class TaskForm(TaskFormBase): context = QuerySelectField(get_label="name") TaskForm.context.creation_counter = -1 ContextForm = make_global( model_form_type(Context, exclude=[ "id", "position", "workspace_id", "created_datetime", "tasks", "workspace" ])) TagForm = make_global( model_form_type(Tag, exclude=[ "id", "position", "workspace_id", "created_datetime", "tasks", "workspace" ])) PreferencesBaseForm = make_global( model_form_type( User, exclude=[ "id", "username", "password", "salt", "created_datetime", "feature_idx", "tutorial_idx", "workspaces" ], field_args={ "do_notify": { "label": _("Send nightly mail notifications"), "validators": [ RequireField( "email", _("You need to supply your e-mail address to use this feature!" )) ], }, "email": { "label": _("E-mail"), }, })) @make_global class PreferencesForm(PreferencesBaseForm): locale = SelectField(_("Language"), choices=locale_choices) pwd1 = PasswordField( _('New Password'), [EqualTo('pwd2', message=_('Passwords must match'))]) pwd2 = PasswordField(_('Repeat Password')) from dtg.transtools import _
def workspace_tasks(workspace): action = request.form.get("action", None) if action is None: context = Context.query.filter_by(id=int(request.form.get("selected_context")), workspace=workspace).first() return jsonify({"data": get_filtered_task_dicts(workspace, context.tasks, request.form), "seqid": workspace.seqid}) else: if action == "create": context = Context.query.filter_by(id=int(request.form.get("selected_context")), workspace=workspace).first() assert context is not None summary = request.form.get("summary") + " " tags = [] if ":" in summary: contextname, suffix = summary.split(":", 1) newcontext = Context.query.filter_by(name=contextname, workspace=workspace).first() if newcontext is not None: context = newcontext summary = suffix.lstrip() if "#" in summary: for tag in Tag.query.filter_by(workspace=workspace).all(): search_for = ("#%s " % (tag.name, )) if search_for in summary: tags.append(tag) summary = summary.replace(search_for, "") summary = summary.rstrip() task = Task(summary, "", tags=tags) context.tasks.insert(0, task) db.session.add(task) flash(_("Created task '%s'", task.summary)) db.session.commit() return jsonify({"data": task.id}) elif action == "typeahead": items = [] prefix = request.form.get("summary") if " #" in prefix: start, end = prefix.rsplit(" #", 1) for tag in Tag.query.filter_by(workspace=workspace).all(): if tag.name.lower().startswith(end.lower()) and " #%s " % (tag.name, ) not in (prefix + " "): items.append("%s #%s" % (start, tag.name)) elif ":" not in prefix and prefix: for context in Context.query.filter_by(workspace=workspace).all(): if context.name.lower().startswith(prefix.lower()): items.append("%s: " % (context.name, )) return jsonify({"list": items}) task = Task.query.filter_by(id=int(request.form.get("id"))).first() if task.context.workspace != workspace: return render_error(_("Wrong workspace"), True) if action == "move": context = task.context previous_task = Task.query.filter_by(id=int(request.form.get("previous_id"))).first() old_index = context.tasks.index(task) if previous_task is None: new_index = 0 else: if previous_task.context.workspace != workspace: return render_error(_("Wrong workspace"), True) new_index = context.tasks.index(previous_task) + 1 if old_index < new_index: new_index -= 1 context.tasks.pop(old_index) context.tasks.insert(new_index, task) db.session.commit() return jsonify({}) query = Context.query.filter_by(workspace=workspace) edittitle = unicode(_("Edit task") if task else _("Add task")) if action == "editinitial": form = TaskForm(obj=task) form.context.query = query return jsonify({"title": edittitle, "body": render_template("edit.html", form=form, type="task", task=task)}) elif action == "edit": form = TaskForm(request.form, obj=task) form.context.query = query if form.validate(): form.populate_obj(task) if task.recur_reschedule_this: task.reschedule() flash(_("Saved task '%s'", task.summary)) db.session.commit() return jsonify({}) return jsonify({"status": "AGAIN", "title": edittitle, "body": render_template("edit.html", form=form, type="task", task=task)}) elif action == "delete": flash(_("Deleted task '%s'", task.summary)) task.delete() db.session.commit() return jsonify({}) elif action == "remove_tag": tag = Tag.query.filter_by(id=int(request.form.get("tag_id")), workspace=workspace).first() task.tags.remove(tag) db.session.commit() return jsonify({}) elif action == "add_tag": tag = Tag.query.filter_by(id=int(request.form.get("tag_id")), workspace=workspace).first() task.tags.append(tag) db.session.commit() return jsonify({}) elif action == "rename": task.summary = request.form.get("summary", "") flash(_("Changed summary to '%s'", (task.summary, ))) db.session.commit() return jsonify({}) elif action == "togglecomplete": task.completed = not task.completed if task.completed: flash(_("Marked task '%s' as completed", (task.summary, ))) else: flash(_("Reopened task '%s'", (task.summary, ))) db.session.commit() return jsonify({}) elif action == "postpone": earliest_due = date.today() if not task.due: task_due = earliest_due flash(_("Set due date to tomorrow")) else: task_due = task.due flash(_("Postponed task by one day")) task.due = max(earliest_due, task_due + timedelta(days=1)) db.session.commit() return jsonify({}) return render_error(_("Unknown action"), True)
def translate(): txt = request.form.get("txt") return jsonify({"txt": unicode(_(txt))})
def innerfunc(workspace, *args, **kwargs): workspace = Workspace.query.filter_by(name=workspace, owner=request.user).first() if workspace is None: return render_error(_("Workspace not found")), 404 request.workspace = workspace return func(workspace, *args, **kwargs)
def __call__(self, text=None, **kwargs): from dtg.transtools import _ kwargs['for'] = self.field_id attributes = widgets.html_params(**kwargs) return widgets.HTMLString(u'<label %s>%s</label>' % (attributes, unicode(_(text or self.text))))