Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ ANYTIME_STATUS=['To Do', 'Open', 'New']
COMPLETED_STATUS=['Done', 'Closed', 'Resolved']
```

### Missing Tickets

When tickets are deleted or reassigned in Jira, they may stop being returned in the JQL response.

To handle this, you can enable the following option to mark missing tickets as canceled in Things:

```ini
CANCEL_MISSING_TICKETS=true
```

If a cancelled ticket later reappears, it will be un-canceled.

## Status Mapping Logic

The app maps JIRA ticket statuses to Things 3 scheduling areas:
Expand Down
8 changes: 6 additions & 2 deletions config.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ TODAY_STATUS=["In Progress", "Active", "Doing"]
# Tickets with these statuses go to "Anytime" in Things (default for unlisted statuses)
ANYTIME_STATUS=["To Do", "Open", "Ready", "Dev Ready"]

# Tickets with these statuses go to "Someday" in Things
# Tickets with these statuses go to "Someday" in Things
SOMEDAY_STATUS=["Backlog", "Future", "Product Backlog", "Icebox"]

# Tickets with these statuses are marked as completed in Things
COMPLETED_STATUS=["Done", "Closed", "Resolved", "Completed"]
COMPLETED_STATUS=["Done", "Closed", "Resolved", "Completed"]

# Should missing tickets be marked as canceled in Things?
# For example, if a ticket is reassigned or deleted in Jira, it will not be returned in the JQL fetch.
CANCEL_MISSING_TICKETS=false
44 changes: 41 additions & 3 deletions database.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class JiraTicket:
status: str
issue_type: str = None
things_id: str = None
present_in_last_fetch: bool = True
last_updated: str = None

class DatabaseManager:
Expand Down Expand Up @@ -51,10 +52,21 @@ def _init_db(self) -> None:
issue_type TEXT,
things_id TEXT,
added_to_db TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
present_in_last_fetch BOOLEAN NOT NULL DEFAULT 1,
synced_to_things TEXT DEFAULT 'not synced' CHECK(synced_to_things IN ('synced', 'not synced', 'unknown')),
last_updated TIMESTAMP
)
''')

# We don't have a full schema migration system, so this just attempts to add
# any new columns from after the 1.0 release and skips if they already exist.
for field in ['present_in_last_fetch BOOLEAN NOT NULL DEFAULT 1']:
try:
cursor.execute(f"ALTER TABLE jira_tickets ADD COLUMN {field};")
except sqlite3.OperationalError as e:
if "duplicate column name" not in str(e):
raise

conn.commit()
logging.info("Database initialization complete")

Expand Down Expand Up @@ -116,7 +128,7 @@ def get_all_tickets(self) -> List[JiraTicket]:
with self.get_connection() as conn:
cursor = conn.cursor()
logging.debug("Retrieving all tickets from database")
cursor.execute('SELECT ticket_id, summary, description, has_subtasks, status, issue_type, things_id, last_updated FROM jira_tickets')
cursor.execute('SELECT ticket_id, summary, description, has_subtasks, status, issue_type, things_id, present_in_last_fetch, last_updated FROM jira_tickets')
return [JiraTicket(*row) for row in cursor.fetchall()]

def get_unsynced_tickets(self) -> List[JiraTicket]:
Expand All @@ -135,8 +147,34 @@ def get_ticket_by_id(self, ticket_id: str) -> Optional[JiraTicket]:
"""Retrieve a specific ticket by its ID. Returns None if not found."""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT ticket_id, summary, description, has_subtasks, status, issue_type, things_id, last_updated FROM jira_tickets WHERE ticket_id = ?', (ticket_id,))
cursor.execute('SELECT ticket_id, summary, description, has_subtasks, status, issue_type, things_id, present_in_last_fetch, last_updated FROM jira_tickets WHERE ticket_id = ?', (ticket_id,))
row = cursor.fetchone()
if row:
return JiraTicket(*row)
return None
return None

def mark_present(self, all_issues: List[JiraTicket]):
"""Mark the given issues as present in the last fetch, updating sync status as necessary."""
with self.get_connection() as conn:
ticket_ids = [ticket.ticket_id for ticket in all_issues]
if not ticket_ids:
return

cursor = conn.cursor()

cursor.execute(
'''
UPDATE jira_tickets
SET present_in_last_fetch = ticket_id IN ({}),
synced_to_things = CASE
WHEN synced_to_things = 'synced' AND present_in_last_fetch != (ticket_id IN ({})) THEN 'not synced'
ELSE synced_to_things
END
'''.format(
','.join('?' * len(ticket_ids)),
','.join('?' * len(ticket_ids))
),
ticket_ids + ticket_ids
)

conn.commit()
13 changes: 10 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def update_db(db_manager: DatabaseManager, jira_client: JiraClient, config: dict

# Save ticket (will only update DB if changes detected)
db_manager.save_ticket(issue)


db_manager.mark_present(issues)

# Report processing summary
total_processed = added_count + updated_count + unchanged_count
logging.info(f"Database update complete: {added_count} tickets added, {updated_count} tickets updated, {unchanged_count} unchanged (Total processed: {total_processed})")
Expand Down Expand Up @@ -230,11 +232,16 @@ def _build_things_task_data(ticket: JiraTicket, config: dict, today_status: set,
kwargs['when'] = 'someday'
else:
kwargs['when'] = 'anytime'

# Mark as completed/canceled if needed
cancel_missing_tickets = config.get('CANCEL_MISSING_TICKETS', 'false').lower() == 'true'

# Mark as completed if needed
if ticket.status in completed_status:
kwargs['completed'] = True

kwargs['canceled'] = False
elif cancel_missing_tickets:
kwargs['canceled'] = not ticket.present_in_last_fetch

return kwargs

def sync_to_things(db_manager: DatabaseManager, config: dict, today_status: set, anytime_status: set, someday_status: set, completed_status: set):
Expand Down