Skip to content

Commit

Permalink
Merge branch '1.0'
Browse files Browse the repository at this point in the history
Conflicts:
	version
  • Loading branch information
agateau committed Mar 28, 2016
2 parents 28f72a3 + 5e7f461 commit 6ff18ff
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 28 deletions.
6 changes: 6 additions & 0 deletions NEWS
@@ -1,3 +1,9 @@
v1.0.2 2016/03/28

- Use a more portable way to get the terminal size. This makes it possible to use Yokadi inside Android terminal emulators like Termux
- Sometimes the task lock used to prevent editing the same task description from multiple Yokadi instances were not correctly released
- Deleting a keyword from the database caused a crash when a t_list returned tasks which previously contained this keyword

v1.0.1 2015/12/03

- User changes:
Expand Down
2 changes: 1 addition & 1 deletion doc/release.md
Expand Up @@ -43,7 +43,7 @@ Update `NEWS` file (add changes, check release date)
Bump version number

echo $version > version
git commit version -m "Getting ready for $version"
git commit NEWS version -m "Getting ready for $version"

### Common

Expand Down
38 changes: 38 additions & 0 deletions update/update8to9
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
"""
Update from version 8 to version 9 of Yokadi DB
- Delete invalid TaskKeyword rows
@author: Aurélien Gâteau <mail@agateau.com>
@license: GPL v3 or newer
"""
import sys

from sqlalchemy import create_engine


# TODO: Current db version is 8, but this file has been created so that when we
# create version 9, we can do the work which is currently done in
# db.deleteInvalidTaskKeywordRows() and remove db.deleteInvalidTaskKeywordRows()
#
# The cleanup is done at startup for db version 8 because it is a fast,
# data-only cleanup so it is not worth requiring users to update their database
# schema.


def deleteInvalidTaskKeywordRows(conn):
conn.execute('delete from task_keyword where task_id is null or keyword_id is null')


def main():
uri = 'sqlite:///' + sys.argv[1]
engine = create_engine(uri)
with engine.begin() as conn:
deleteInvalidTaskKeywordRows(conn)


if __name__ == "__main__":
main()
# vi: ts=4 sw=4 et
24 changes: 14 additions & 10 deletions yokadi/core/db.py
Expand Up @@ -15,7 +15,7 @@
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Enum, ForeignKey
from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Enum, ForeignKey, or_


try:
Expand Down Expand Up @@ -69,22 +69,18 @@ class Keyword(Base):
id = Column(Integer, primary_key=True)
name = Column(Unicode, unique=True)
tasks = association_proxy("taskKeywords", "task")
projects = association_proxy("projectKeywords", "project")
taskKeywords = relationship("TaskKeyword", cascade="all", backref="keyword")

def __repr__(self):
return self.name

def getTasks(self):
return [taskKeyword.task for taskKeyword in self.taskKeywords]


class TaskKeyword(Base):
__tablename__ = "task_keyword"
id = Column(Integer, primary_key=True)
taskId = Column("task_id", Integer, ForeignKey("task.id"))
keywordId = Column("keyword_id", Integer, ForeignKey("keyword.id"))
taskId = Column("task_id", Integer, ForeignKey("task.id"), nullable=False)
keywordId = Column("keyword_id", Integer, ForeignKey("keyword.id"), nullable=False)
value = Column(Integer, default=None)
keyword = relationship("Keyword", backref="taskKeywords")


class Task(Base):
Expand All @@ -97,7 +93,7 @@ class Task(Base):
description = Column(Unicode, default="", nullable=False)
urgency = Column(Integer, default=0, nullable=False)
status = Column(Enum("new", "started", "done"), default="new")
projectId = Column("project_id", Integer, ForeignKey("project.id"))
projectId = Column("project_id", Integer, ForeignKey("project.id"), nullable=False)
taskKeywords = relationship("TaskKeyword", cascade="all", backref="task")
recurrenceId = Column("recurrence_id", Integer, ForeignKey("recurrence.id"), default=None)
recurrence = relationship("Recurrence", cascade="all", backref="task")
Expand Down Expand Up @@ -204,7 +200,7 @@ class Config(Base):
class TaskLock(Base):
__tablename__ = "task_lock"
id = Column(Integer, primary_key=True)
taskId = Column("task_id", Integer, ForeignKey("task.id"), unique=True)
taskId = Column("task_id", Integer, ForeignKey("task.id"), unique=True, nullable=False)
pid = Column(Integer, default=None)
updateDate = Column("update_date", DateTime, default=None)

Expand Down Expand Up @@ -321,4 +317,12 @@ def setDefaultConfig():
if session.query(Config).filter_by(name=name).count() == 0:
session.add(Config(name=name, value=value[0], system=value[1], desc=value[2]))


def deleteInvalidTaskKeywordRows():
# TODO: Remove this function when we migrate to database version 9. See
# update8to9.
session = getSession()
filters = or_(TaskKeyword.taskId == None, TaskKeyword.keywordId == None)
session.query(TaskKeyword).filter(filters).delete(synchronize_session=False)
session.commit()
# vi: ts=4 sw=4 et
24 changes: 17 additions & 7 deletions yokadi/core/dbutils.py
Expand Up @@ -172,22 +172,32 @@ def _getLock(self):
except NoResultFound:
return None

def acquire(self):
def acquire(self, pid=None, now=None):
"""Acquire a lock for that task and remove any previous stale lock"""
if now is None:
now = datetime.now()
if pid is None:
pid = os.getpid()

lock = self._getLock()
if lock:
if lock.updateDate < datetime.now() - 2 * timedelta(seconds=tui.MTIME_POLL_INTERVAL):
# Stale lock, removing
self.session.delete(lock)
if lock.updateDate < now - 2 * timedelta(seconds=tui.MTIME_POLL_INTERVAL):
# Stale lock, reusing it
lock.pid = pid
lock.updateDate = now
else:
raise YokadiException("Task %s is already locked by process %s" % (lock.task.id, lock.pid))
self.session.add(TaskLock(task=self.task, pid=os.getpid(), updateDate=datetime.now()))
else:
# Create a lock
self.session.add(TaskLock(task=self.task, pid=pid, updateDate=now))
self.session.commit()

def update(self):
def update(self, now=None):
"""Update lock timestamp to avoid it to expire"""
if now is None:
now = datetime.now()
lock = self._getLock()
lock.updateDate = datetime.now()
lock.updateDate = now
self.session.merge(lock)
self.session.commit()

Expand Down
19 changes: 19 additions & 0 deletions yokadi/tests/dbutilstestcase.py
Expand Up @@ -7,6 +7,8 @@

import unittest

from datetime import datetime

import testutils

from yokadi.core import dbutils, db
Expand Down Expand Up @@ -54,4 +56,21 @@ def testGetKeywordFromName(self):
self.assertRaises(YokadiException, dbutils.getKeywordFromName, "")
self.assertRaises(YokadiException, dbutils.getKeywordFromName, "foo")
self.assertEqual(k1, dbutils.getKeywordFromName("k1"))

def testTaskLockManagerStaleLock(self):
tui.addInputAnswers("y")
t1 = dbutils.addTask("x", "t1", {})
taskLockManager = dbutils.TaskLockManager(t1)

# Lock the task
taskLockManager.acquire(pid=1, now=datetime(2014, 1, 1))
lock1 = taskLockManager._getLock()
self.assertEqual(lock1.pid, 1)

# Try to lock again, the stale lock should get reused
taskLockManager.acquire(pid=2, now=datetime(2015, 1, 1))
lock2 = taskLockManager._getLock()
self.assertEqual(lock1.id, lock2.id)
self.assertEqual(lock2.pid, 2)

# vi: ts=4 sw=4 et
10 changes: 10 additions & 0 deletions yokadi/tests/keywordtestcase.py
Expand Up @@ -59,4 +59,14 @@ def testKEditCannotMerge(self):
self.assertTrue("k2" in kwDict)

dbutils.getKeywordFromName("k1")

def testKRemove(self):
t1 = dbutils.addTask("x", "t1", dict(k1=12, k2=None), interactive=False)
tui.addInputAnswers("y")
self.cmd.do_k_remove("k1")
kwDict = t1.getKeywordDict()
self.assertFalse("k1" in kwDict)
self.assertTrue("k2" in kwDict)
taskKeyword = self.session.query(db.TaskKeyword).filter_by(taskId=t1.id).one()
self.assertEqual(taskKeyword.keyword.name, "k2")
# vi: ts=4 sw=4 et
1 change: 1 addition & 0 deletions yokadi/ycli/main.py
Expand Up @@ -232,6 +232,7 @@ def main():
if args.createOnly:
return
db.setDefaultConfig() # Set default config parameters
db.deleteInvalidTaskKeywordRows()

cmd = YokadiCmd()

Expand Down
14 changes: 4 additions & 10 deletions yokadi/ycli/tui.py
Expand Up @@ -15,6 +15,7 @@
import unicodedata
import re
import locale
import shutil
from getpass import getpass

from yokadi.ycli import colors as C
Expand Down Expand Up @@ -242,15 +243,8 @@ def clearInputAnswers():


def getTermWidth():
"""Gets the terminal width. Works only on Unix system.
@return: terminal width or "120" is system not supported
Kindly borrowed from pysql code"""
width = 120
if os.name == "posix":
result = subprocess.Popen(["tput", "cols"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0]
result = result.strip()
if result.isdigit():
width = int(result)
return width
"""Gets the terminal width"""
size = shutil.get_terminal_size()
return size.columns

# vi: ts=4 sw=4 et

0 comments on commit 6ff18ff

Please sign in to comment.