forked from kenkeiras/org-rw
Add support for planning info update.
This commit is contained in:
parent
d71f98f4b9
commit
1d3b4c187b
143
org_rw/org_rw.py
143
org_rw/org_rw.py
@ -54,10 +54,8 @@ NODE_PROPERTIES_RE = re.compile(
|
|||||||
r"^(?P<indentation>\s*):(?P<key>[^+:]+)(?P<plus>\+)?:(?P<spacing>\s*)(?P<value>.+)$"
|
r"^(?P<indentation>\s*):(?P<key>[^+:]+)(?P<plus>\+)?:(?P<spacing>\s*)(?P<value>.+)$"
|
||||||
)
|
)
|
||||||
RAW_LINE_RE = re.compile(r"^\s*([^\s#:*]|$)")
|
RAW_LINE_RE = re.compile(r"^\s*([^\s#:*]|$)")
|
||||||
BASE_TIME_STAMP_RE = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})( ?(?P<dow>[^ ]+))?( (?P<start_hour>\d{1,2}):(?P<start_minute>\d{1,2})(--(?P<end_hour>\d{1,2}):(?P<end_minute>\d{1,2}))?)?"
|
BASE_TIME_STAMP_RE = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})( ?(?P<dow>[^ ]+))?( (?P<start_hour>\d{1,2}):(?P<start_minute>\d{1,2})(--(?P<end_hour>\d{1,2}):(?P<end_minute>\d{1,2}))?)?(?P<repetition> (?P<rep_mark>(\+|\+\+|\.\+|-|--))(?P<rep_value>\d+)(?P<rep_unit>[hdwmy]))?"
|
||||||
CLEAN_TIME_STAMP_RE = (
|
CLEAN_TIME_STAMP_RE = r"\d{4}-\d{2}-\d{2}( ?([^ ]+))?( (\d{1,2}):(\d{1,2})(--(\d{1,2}):(\d{1,2}))?)?( (\+|\+\+|\.\+|-|--)\d+[hdwmy])?"
|
||||||
r"\d{4}-\d{2}-\d{2}( ?([^ ]+))?( (\d{1,2}):(\d{1,2})(--(\d{1,2}):(\d{1,2}))?)?"
|
|
||||||
)
|
|
||||||
|
|
||||||
ACTIVE_TIME_STAMP_RE = re.compile(r"<{}>".format(BASE_TIME_STAMP_RE))
|
ACTIVE_TIME_STAMP_RE = re.compile(r"<{}>".format(BASE_TIME_STAMP_RE))
|
||||||
INACTIVE_TIME_STAMP_RE = re.compile(r"\[{}\]".format(BASE_TIME_STAMP_RE))
|
INACTIVE_TIME_STAMP_RE = re.compile(r"\[{}\]".format(BASE_TIME_STAMP_RE))
|
||||||
@ -208,6 +206,20 @@ class Headline:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if m := PLANNING_RE.match(planning_line.get_raw()):
|
if m := PLANNING_RE.match(planning_line.get_raw()):
|
||||||
|
self._planning_indendation = m.group("indentation")
|
||||||
|
self._planning_order = []
|
||||||
|
|
||||||
|
keywords = ["SCHEDULED", "CLOSED", "DEADLINE"]
|
||||||
|
plan = planning_line.get_raw().split("\n")[0]
|
||||||
|
indexes = [(kw, plan.find(kw)) for kw in keywords]
|
||||||
|
|
||||||
|
self._planning_order = [
|
||||||
|
kw
|
||||||
|
for (kw, idx) in sorted(
|
||||||
|
filter(lambda v: v[1] >= 0, indexes), key=lambda v: v[1]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
if scheduled := m.group("scheduled"):
|
if scheduled := m.group("scheduled"):
|
||||||
self.scheduled = time_from_str(scheduled)
|
self.scheduled = time_from_str(scheduled)
|
||||||
if closed := m.group("closed"):
|
if closed := m.group("closed"):
|
||||||
@ -215,6 +227,37 @@ class Headline:
|
|||||||
if deadline := m.group("deadline"):
|
if deadline := m.group("deadline"):
|
||||||
self.deadline = time_from_str(deadline)
|
self.deadline = time_from_str(deadline)
|
||||||
|
|
||||||
|
# Remove from contents
|
||||||
|
self._remove_element_in_line(start_line + 1)
|
||||||
|
|
||||||
|
def get_planning_line(self):
|
||||||
|
if self.scheduled is None and self.closed is None and self.deadline is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
contents = [self._planning_indendation]
|
||||||
|
|
||||||
|
for el in self._planning_order:
|
||||||
|
if el == "SCHEDULED" and self.scheduled is not None:
|
||||||
|
contents.append("SCHEDULED: {} ".format(self.scheduled.to_raw()))
|
||||||
|
|
||||||
|
elif el == "CLOSED" and self.closed is not None:
|
||||||
|
contents.append("CLOSED: {} ".format(self.closed.to_raw()))
|
||||||
|
|
||||||
|
elif el == "DEADLINE" and self.deadline is not None:
|
||||||
|
contents.append("DEADLINE: {} ".format(self.deadline.to_raw()))
|
||||||
|
|
||||||
|
# Consider elements added (not present on planning order)
|
||||||
|
if ("SCHEDULED" not in self._planning_order) and (self.scheduled is not None):
|
||||||
|
contents.append("SCHEDULED: {} ".format(self.scheduled.to_raw()))
|
||||||
|
|
||||||
|
if ("CLOSED" not in self._planning_order) and (self.closed is not None):
|
||||||
|
contents.append("CLOSED: {} ".format(self.closed.to_raw()))
|
||||||
|
|
||||||
|
if ("DEADLINE" not in self._planning_order) and (self.deadline is not None):
|
||||||
|
contents.append("DEADLINE: {} ".format(self.deadline.to_raw()))
|
||||||
|
|
||||||
|
return "".join(contents).rstrip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clock(self):
|
def clock(self):
|
||||||
times = []
|
times = []
|
||||||
@ -279,6 +322,27 @@ class Headline:
|
|||||||
if linenum == s_lnum:
|
if linenum == s_lnum:
|
||||||
return ("structural", struc)
|
return ("structural", struc)
|
||||||
|
|
||||||
|
def _remove_element_in_line(self, linenum):
|
||||||
|
found = None
|
||||||
|
for i, line in enumerate(self.contents):
|
||||||
|
if linenum == line.linenum:
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
|
||||||
|
assert found is not None
|
||||||
|
el = self.contents[found]
|
||||||
|
assert isinstance(el, Text)
|
||||||
|
|
||||||
|
raw = el.get_raw()
|
||||||
|
if "\n" not in raw:
|
||||||
|
# Remove the element found
|
||||||
|
self.contents.pop(found)
|
||||||
|
else:
|
||||||
|
# Remove the first line
|
||||||
|
self.contents[found] = parse_content_block(
|
||||||
|
[RawLine(self.contents[found].linenum + 1, raw.split("\n", 1)[1])]
|
||||||
|
)
|
||||||
|
|
||||||
def get_structural_end_after(self, linenum):
|
def get_structural_end_after(self, linenum):
|
||||||
for (s_lnum, struc) in self.structural:
|
for (s_lnum, struc) in self.structural:
|
||||||
if s_lnum > linenum and struc.strip().upper() == ":END:":
|
if s_lnum > linenum and struc.strip().upper() == ":END:":
|
||||||
@ -376,9 +440,61 @@ Property = collections.namedtuple(
|
|||||||
|
|
||||||
# @TODO How are [YYYY-MM-DD HH:mm--HH:mm] and ([... HH:mm]--[... HH:mm]) differentiated ?
|
# @TODO How are [YYYY-MM-DD HH:mm--HH:mm] and ([... HH:mm]--[... HH:mm]) differentiated ?
|
||||||
# @TODO Consider recurrence annotations
|
# @TODO Consider recurrence annotations
|
||||||
Timestamp = collections.namedtuple(
|
class Timestamp:
|
||||||
"Timestamp", ("active", "year", "month", "day", "dow", "hour", "minute")
|
def __init__(self, active, year, month, day, dow, hour, minute, repetition=None):
|
||||||
)
|
self.active = active
|
||||||
|
self._year = year
|
||||||
|
self._month = month
|
||||||
|
self._day = day
|
||||||
|
self.dow = dow
|
||||||
|
self.hour = hour
|
||||||
|
self.minute = minute
|
||||||
|
self.repetition = repetition
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Timestamp):
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
(self.active == other.active)
|
||||||
|
and (self.year == other.year)
|
||||||
|
and (self.month == other.month)
|
||||||
|
and (self.day == other.day)
|
||||||
|
and (self.dow == other.dow)
|
||||||
|
and (self.hour == other.hour)
|
||||||
|
and (self.minute == other.minute)
|
||||||
|
and (self.repetition == other.repetition)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return timestamp_to_string(self)
|
||||||
|
|
||||||
|
# Properties whose modification changes the Day-Of-Week
|
||||||
|
@property
|
||||||
|
def year(self):
|
||||||
|
return self._year
|
||||||
|
|
||||||
|
@year.setter
|
||||||
|
def year(self, value):
|
||||||
|
self._year = value
|
||||||
|
self.dow = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def month(self):
|
||||||
|
return self._month
|
||||||
|
|
||||||
|
@month.setter
|
||||||
|
def month(self, value):
|
||||||
|
self._month = value
|
||||||
|
self.dow = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def day(self):
|
||||||
|
return self._day
|
||||||
|
|
||||||
|
@day.setter
|
||||||
|
def day(self, value):
|
||||||
|
self._day = value
|
||||||
|
self.dow = None
|
||||||
|
|
||||||
|
|
||||||
class DelimiterLineType(Enum):
|
class DelimiterLineType(Enum):
|
||||||
@ -495,13 +611,17 @@ def parse_org_time(value):
|
|||||||
m.group("dow"),
|
m.group("dow"),
|
||||||
int(m.group("start_hour")) if m.group("start_hour") else None,
|
int(m.group("start_hour")) if m.group("start_hour") else None,
|
||||||
int(m.group("start_minute")) if m.group("start_minute") else None,
|
int(m.group("start_minute")) if m.group("start_minute") else None,
|
||||||
|
m.group("repetition").strip() if m.group("repetition") else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrgTime:
|
class OrgTime:
|
||||||
def __init__(self, ts: Timestamp):
|
def __init__(self, ts: Timestamp):
|
||||||
|
assert ts is not None
|
||||||
self.time = ts
|
self.time = ts
|
||||||
self.date = date(ts.year, ts.month, ts.day)
|
|
||||||
|
def to_raw(self):
|
||||||
|
return timestamp_to_string(self.time)
|
||||||
|
|
||||||
|
|
||||||
def time_from_str(s: str):
|
def time_from_str(s: str):
|
||||||
@ -526,6 +646,9 @@ def timestamp_to_string(ts: Timestamp):
|
|||||||
else:
|
else:
|
||||||
base = date
|
base = date
|
||||||
|
|
||||||
|
if ts.repetition:
|
||||||
|
base = base + " " + ts.repetition
|
||||||
|
|
||||||
if ts.active:
|
if ts.active:
|
||||||
return "<{}>".format(base)
|
return "<{}>".format(base)
|
||||||
else:
|
else:
|
||||||
@ -1079,6 +1202,10 @@ class OrgDoc:
|
|||||||
"spacing"
|
"spacing"
|
||||||
) + headline.title + tags
|
) + headline.title + tags
|
||||||
|
|
||||||
|
planning = headline.get_planning_line()
|
||||||
|
if planning is not None:
|
||||||
|
yield planning
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
KW_T = 0
|
KW_T = 0
|
||||||
CONTENT_T = 1
|
CONTENT_T = 1
|
||||||
|
@ -4,7 +4,7 @@ import unittest
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from datetime import datetime as DT
|
from datetime import datetime as DT
|
||||||
|
|
||||||
from org_rw import dumps, load, loads
|
from org_rw import Timestamp, dumps, load, loads
|
||||||
|
|
||||||
from utils.assertions import (BOLD, CODE, HL, ITALIC, SPAN, STRIKE, UNDERLINED,
|
from utils.assertions import (BOLD, CODE, HL, ITALIC, SPAN, STRIKE, UNDERLINED,
|
||||||
VERBATIM, WEB_LINK, Doc, Tokens)
|
VERBATIM, WEB_LINK, Doc, Tokens)
|
||||||
@ -399,6 +399,37 @@ class TestSerde(unittest.TestCase):
|
|||||||
doc = loads(orig)
|
doc = loads(orig)
|
||||||
|
|
||||||
hl = doc.getTopHeadlines()[0]
|
hl = doc.getTopHeadlines()[0]
|
||||||
self.assertEqual(hl.scheduled.date, date(2020, 12, 12))
|
self.assertEqual(
|
||||||
self.assertEqual(hl.closed.date, date(2020, 12, 13))
|
hl.scheduled.time, Timestamp(True, 2020, 12, 12, "Sáb", None, None)
|
||||||
self.assertEqual(hl.deadline.date, date(2020, 12, 14))
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
hl.closed.time, Timestamp(True, 2020, 12, 13, "Dom", None, None)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
hl.deadline.time, Timestamp(True, 2020, 12, 14, "Lun", None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_info_file_05(self):
|
||||||
|
with open(os.path.join(DIR, "05-dates.org")) as f:
|
||||||
|
orig = f.read()
|
||||||
|
doc = loads(orig)
|
||||||
|
|
||||||
|
hl = doc.getTopHeadlines()[0]
|
||||||
|
hl.scheduled.time.day = 15
|
||||||
|
hl.closed.time.day = 16
|
||||||
|
hl.deadline.time.day = 17
|
||||||
|
|
||||||
|
# Account for removeing 3 days-of-week + 1 space each
|
||||||
|
self.assertEqual(len(dumps(doc)), len(orig) - (4) * 3)
|
||||||
|
doc_updated = loads(dumps(doc))
|
||||||
|
|
||||||
|
hl_up = doc_updated.getTopHeadlines()[0]
|
||||||
|
self.assertEqual(
|
||||||
|
hl.scheduled.time, Timestamp(True, 2020, 12, 15, None, None, None)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
hl.closed.time, Timestamp(True, 2020, 12, 16, None, None, None)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
hl.deadline.time, Timestamp(True, 2020, 12, 17, None, None, None)
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user