Internal
Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6b07bfed | ||
|
|
c51fd846a2 | ||
|
|
fcb5ab5ca3 | ||
|
|
fc1a4844a3 | ||
|
|
7f2096db86 | ||
|
|
ea40beb408 | ||
|
|
123d6af430 | ||
|
|
c686021f96 | ||
|
|
41abdf4074 | ||
|
|
31a0c3c8c5 | ||
|
|
8db9f99701 | ||
|
|
8b71447024 | ||
|
|
f836769ad7 | ||
|
|
d83b1e3091 | ||
|
|
e038e47247 | ||
|
|
d5010580a4 |
@@ -30,6 +30,10 @@ app.conf.beat_schedule = {
|
|||||||
'task': 'notifications.tasks.check_overdue_tasks',
|
'task': 'notifications.tasks.check_overdue_tasks',
|
||||||
'schedule': crontab(hour=8, minute=0), # Daily at 8 AM
|
'schedule': crontab(hour=8, minute=0), # Daily at 8 AM
|
||||||
},
|
},
|
||||||
|
'process-recurring-tasks': {
|
||||||
|
'task': 'notifications.tasks.process_recurring_tasks',
|
||||||
|
'schedule': crontab(minute=0), # Every hour
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'tasks.middleware.AllowMobileAppFramingMiddleware', # Allow mobile app iframe embedding
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ from users.urls import api_urlpatterns as users_api_urls, web_urlpatterns as use
|
|||||||
from tasks.urls import api_urlpatterns as tasks_api_urls, web_urlpatterns as tasks_web_urls
|
from tasks.urls import api_urlpatterns as tasks_api_urls, web_urlpatterns as tasks_web_urls
|
||||||
from sync.urls import api_urlpatterns as sync_api_urls
|
from sync.urls import api_urlpatterns as sync_api_urls
|
||||||
from notifications.urls import api_urlpatterns as notifications_api_urls
|
from notifications.urls import api_urlpatterns as notifications_api_urls
|
||||||
|
from tasks.views_debug import debug_user_agent
|
||||||
from .views import api_root
|
from .views import api_root
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Admin
|
# Admin
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
|
||||||
|
# Debug endpoint (remove in production)
|
||||||
|
path('debug/user-agent/', debug_user_agent, name='debug-user-agent'),
|
||||||
|
|
||||||
# API root
|
# API root
|
||||||
path('api/', api_root, name='api-root'),
|
path('api/', api_root, name='api-root'),
|
||||||
|
|
||||||
|
|||||||
@@ -236,3 +236,51 @@ def schedule_task_reminder(task_id):
|
|||||||
task=task,
|
task=task,
|
||||||
remind_at=remind_at
|
remind_at=remind_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def process_recurring_tasks():
|
||||||
|
"""
|
||||||
|
Process completed recurring tasks and create next instances.
|
||||||
|
This runs periodically as a safety net to catch any tasks that weren't
|
||||||
|
automatically handled when marked as completed.
|
||||||
|
Runs every hour via Celery Beat.
|
||||||
|
"""
|
||||||
|
from tasks.models import Task
|
||||||
|
|
||||||
|
# Find completed recurring tasks that don't have a next instance created yet
|
||||||
|
# We look for tasks completed in the last 24 hours to avoid reprocessing old tasks
|
||||||
|
yesterday = timezone.now() - timedelta(days=1)
|
||||||
|
|
||||||
|
completed_recurring_tasks = Task.objects.filter(
|
||||||
|
status='completed',
|
||||||
|
recurrence__in=['daily', 'weekly', 'biweekly', 'monthly', 'yearly', 'custom'],
|
||||||
|
completed_at__gte=yesterday
|
||||||
|
).exclude(recurrence='none')
|
||||||
|
|
||||||
|
tasks_created = 0
|
||||||
|
for task in completed_recurring_tasks:
|
||||||
|
# Check if a next recurrence already exists
|
||||||
|
# Look for pending tasks with the same title, user, and recurrence pattern
|
||||||
|
next_due_date = task.calculate_next_due_date()
|
||||||
|
if next_due_date:
|
||||||
|
# Check if we already created this recurrence
|
||||||
|
existing = Task.objects.filter(
|
||||||
|
user=task.user,
|
||||||
|
title=task.title,
|
||||||
|
due_date=next_due_date,
|
||||||
|
recurrence=task.recurrence,
|
||||||
|
status='pending'
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
# Create the next recurrence
|
||||||
|
new_task = task.create_next_recurrence()
|
||||||
|
if new_task:
|
||||||
|
tasks_created += 1
|
||||||
|
logger.info(f"Created recurring task: {new_task.title} (due: {new_task.due_date})")
|
||||||
|
|
||||||
|
if tasks_created > 0:
|
||||||
|
logger.info(f"Created {tasks_created} recurring task instances")
|
||||||
|
|
||||||
|
return tasks_created
|
||||||
|
|||||||
+176
-26
@@ -857,6 +857,34 @@ a:hover {
|
|||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar toggle button (hidden on desktop, shown on mobile) */
|
||||||
|
.sidebar-toggle {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile overlay - removed dark background, sidebar slides over content */
|
||||||
|
.mobile-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-overlay.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Mobile Responsive
|
Mobile Responsive
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -886,19 +914,53 @@ a:hover {
|
|||||||
--sidebar-width: 0;
|
--sidebar-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-layout {
|
/* Override both app-layout variants to use single column */
|
||||||
|
.app-layout,
|
||||||
|
.app-layout.detail-closed {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Explicit grid positioning for mobile */
|
||||||
|
.app-header {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force task pane into the single column with full width */
|
||||||
|
.task-pane {
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-user span {
|
||||||
|
display: none; /* Hide email on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header-actions {
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-brand {
|
||||||
|
font-size: 1.1rem; /* Slightly smaller on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar drawer - improved positioning */
|
||||||
.app-sidebar {
|
.app-sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: var(--header-height);
|
top: var(--header-height);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 260px;
|
width: 280px; /* Slightly wider for touch targets */
|
||||||
z-index: 50;
|
z-index: 100; /* Higher z-index */
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform var(--anim-normal);
|
transition: transform 0.3s ease-in-out;
|
||||||
|
background-color: var(--surface);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-sidebar.open {
|
.app-sidebar.open {
|
||||||
@@ -909,40 +971,128 @@ a:hover {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Detail pane mobile */
|
||||||
.detail-pane {
|
.detail-pane {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
|
padding: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
.form-row {
|
.form-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task list mobile optimization */
|
||||||
|
.task-item {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch targets - 44px minimum */
|
||||||
|
.task-item-checkbox {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal on mobile */
|
||||||
|
.modal {
|
||||||
|
width: 95%;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile utility classes */
|
||||||
|
.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-full-width {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar toggle button (visible on mobile) */
|
/* Very small phones breakpoint */
|
||||||
.sidebar-toggle {
|
@media (max-width: 480px) {
|
||||||
|
.app-header {
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-brand {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar {
|
||||||
|
width: 260px; /* Narrower on very small screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smaller buttons on very small screens */
|
||||||
|
.header-user .btn {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle {
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile utility classes base definitions */
|
||||||
|
.mobile-only {
|
||||||
display: none;
|
display: none;
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: var(--space-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle svg {
|
.desktop-only {
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile overlay */
|
|
||||||
.mobile-overlay {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-overlay.visible {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,9 @@ def process_time_entry_changes(user, entry_changes, last_sync_at):
|
|||||||
|
|
||||||
def update_task_from_data(task, data):
|
def update_task_from_data(task, data):
|
||||||
"""Update a task from sync data."""
|
"""Update a task from sync data."""
|
||||||
|
# Track old status to detect completion
|
||||||
|
old_status = task.status
|
||||||
|
|
||||||
task.title = data.get('title', task.title)
|
task.title = data.get('title', task.title)
|
||||||
task.description = data.get('description', task.description)
|
task.description = data.get('description', task.description)
|
||||||
task.status = data.get('status', task.status)
|
task.status = data.get('status', task.status)
|
||||||
@@ -302,6 +305,10 @@ def update_task_from_data(task, data):
|
|||||||
task.recurrence_rule = data.get('recurrence_rule', task.recurrence_rule)
|
task.recurrence_rule = data.get('recurrence_rule', task.recurrence_rule)
|
||||||
task.sort_order = data.get('sort_order', task.sort_order)
|
task.sort_order = data.get('sort_order', task.sort_order)
|
||||||
|
|
||||||
|
# Create next recurrence if task is being marked as completed
|
||||||
|
if old_status != 'completed' and task.status == 'completed' and task.recurrence != 'none':
|
||||||
|
task.create_next_recurrence()
|
||||||
|
|
||||||
task.save()
|
task.save()
|
||||||
|
|
||||||
# Handle tags
|
# Handle tags
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .mobile_app import AllowMobileAppFramingMiddleware
|
||||||
|
|
||||||
|
__all__ = ['AllowMobileAppFramingMiddleware']
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Middleware to allow iframe embedding for the KeepItGoing mobile app.
|
||||||
|
|
||||||
|
The mobile app uses Capacitor WebView which embeds the website in an iframe.
|
||||||
|
This middleware detects requests from the mobile app and removes both
|
||||||
|
X-Frame-Options and Content-Security-Policy frame-ancestors headers to allow
|
||||||
|
iframe embedding, while keeping clickjacking protection for regular web browsers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class AllowMobileAppFramingMiddleware:
|
||||||
|
"""
|
||||||
|
Remove frame-blocking headers for requests from KeepItGoing mobile app.
|
||||||
|
|
||||||
|
The mobile app uses a Capacitor WebView. We detect these requests via
|
||||||
|
User-Agent and remove X-Frame-Options and CSP frame-ancestors headers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
# Check if request is from KeepItGoing mobile app
|
||||||
|
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||||
|
|
||||||
|
# Detect Capacitor/Android WebView patterns
|
||||||
|
is_mobile_app = (
|
||||||
|
'wv' in user_agent.lower() or # Android WebView
|
||||||
|
'CapacitorHttp' in user_agent or
|
||||||
|
'com.firebugit.keepitgoing' in user_agent or
|
||||||
|
('KeepItGoing' in user_agent and 'Mobile' in user_agent)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_mobile_app:
|
||||||
|
# Mobile app: Allow iframe embedding - don't add frame-blocking headers
|
||||||
|
# Remove any existing frame headers
|
||||||
|
if 'X-Frame-Options' in response:
|
||||||
|
del response['X-Frame-Options']
|
||||||
|
if 'Content-Security-Policy' in response:
|
||||||
|
del response['Content-Security-Policy']
|
||||||
|
else:
|
||||||
|
# Regular browsers: Add security headers for clickjacking protection
|
||||||
|
if 'X-Frame-Options' not in response:
|
||||||
|
response['X-Frame-Options'] = 'DENY'
|
||||||
|
|
||||||
|
if 'Content-Security-Policy' not in response:
|
||||||
|
response['Content-Security-Policy'] = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self' 'unsafe-inline'; "
|
||||||
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
|
"img-src 'self' data: https:; "
|
||||||
|
"font-src 'self' data:; "
|
||||||
|
"connect-src 'self'; "
|
||||||
|
"frame-ancestors 'none'; "
|
||||||
|
"base-uri 'self'; "
|
||||||
|
"form-action 'self';"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -174,6 +174,79 @@ class Task(models.Model):
|
|||||||
secs = seconds % 60
|
secs = seconds % 60
|
||||||
return f"{hours}:{minutes:02d}:{secs:02d}"
|
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||||
|
|
||||||
|
def calculate_next_due_date(self, from_date=None):
|
||||||
|
"""
|
||||||
|
Calculate the next due date based on recurrence pattern.
|
||||||
|
Returns None if task doesn't recur or has reached end date.
|
||||||
|
"""
|
||||||
|
if self.recurrence == 'none':
|
||||||
|
return None
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
base_date = from_date or self.due_date
|
||||||
|
if not base_date:
|
||||||
|
from django.utils import timezone
|
||||||
|
base_date = timezone.now().date()
|
||||||
|
|
||||||
|
# Calculate next date based on recurrence type
|
||||||
|
if self.recurrence == 'daily':
|
||||||
|
next_date = base_date + timedelta(days=1)
|
||||||
|
elif self.recurrence == 'weekly':
|
||||||
|
next_date = base_date + timedelta(weeks=1)
|
||||||
|
elif self.recurrence == 'biweekly':
|
||||||
|
next_date = base_date + timedelta(weeks=2)
|
||||||
|
elif self.recurrence == 'monthly':
|
||||||
|
next_date = base_date + relativedelta(months=1)
|
||||||
|
elif self.recurrence == 'yearly':
|
||||||
|
next_date = base_date + relativedelta(years=1)
|
||||||
|
elif self.recurrence == 'custom' and self.recurrence_rule:
|
||||||
|
# TODO: Implement RRULE parsing for custom recurrence
|
||||||
|
# For now, default to weekly
|
||||||
|
next_date = base_date + timedelta(weeks=1)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if we've passed the end date
|
||||||
|
if self.recurrence_end_date and next_date > self.recurrence_end_date:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return next_date
|
||||||
|
|
||||||
|
def create_next_recurrence(self):
|
||||||
|
"""
|
||||||
|
Create the next instance of this recurring task.
|
||||||
|
Returns the new task or None if no recurrence should be created.
|
||||||
|
"""
|
||||||
|
if self.recurrence == 'none':
|
||||||
|
return None
|
||||||
|
|
||||||
|
next_due_date = self.calculate_next_due_date()
|
||||||
|
if not next_due_date:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create new task with same properties
|
||||||
|
new_task = Task.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
parent=self.parent,
|
||||||
|
title=self.title,
|
||||||
|
description=self.description,
|
||||||
|
status='pending',
|
||||||
|
priority=self.priority,
|
||||||
|
due_date=next_due_date,
|
||||||
|
due_time=self.due_time,
|
||||||
|
recurrence=self.recurrence,
|
||||||
|
recurrence_rule=self.recurrence_rule,
|
||||||
|
recurrence_end_date=self.recurrence_end_date,
|
||||||
|
sort_order=self.sort_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy tags
|
||||||
|
new_task.tags.set(self.tags.all())
|
||||||
|
|
||||||
|
return new_task
|
||||||
|
|
||||||
|
|
||||||
class TimeEntry(models.Model):
|
class TimeEntry(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -425,6 +425,9 @@ class TaskDetailView(View):
|
|||||||
task.title = request.POST.get('title', task.title)
|
task.title = request.POST.get('title', task.title)
|
||||||
task.description = request.POST.get('description', '')
|
task.description = request.POST.get('description', '')
|
||||||
|
|
||||||
|
# Track old status to detect completion
|
||||||
|
old_status = task.status
|
||||||
|
|
||||||
# Validate status against allowed choices
|
# Validate status against allowed choices
|
||||||
status = request.POST.get('status', task.status)
|
status = request.POST.get('status', task.status)
|
||||||
valid_statuses = [choice[0] for choice in Task.STATUS_CHOICES]
|
valid_statuses = [choice[0] for choice in Task.STATUS_CHOICES]
|
||||||
@@ -451,6 +454,10 @@ class TaskDetailView(View):
|
|||||||
else:
|
else:
|
||||||
task.recurrence = 'none'
|
task.recurrence = 'none'
|
||||||
|
|
||||||
|
# Create next recurrence if task is being marked as completed
|
||||||
|
if old_status != 'completed' and task.status == 'completed' and task.recurrence != 'none':
|
||||||
|
task.create_next_recurrence()
|
||||||
|
|
||||||
task.save()
|
task.save()
|
||||||
|
|
||||||
# Handle tags
|
# Handle tags
|
||||||
@@ -564,7 +571,13 @@ def task_toggle_status(request, task_id):
|
|||||||
if task.status == 'completed':
|
if task.status == 'completed':
|
||||||
task.status = 'pending'
|
task.status = 'pending'
|
||||||
else:
|
else:
|
||||||
|
# Mark as completed
|
||||||
task.status = 'completed'
|
task.status = 'completed'
|
||||||
|
|
||||||
|
# Create next recurrence if this is a recurring task
|
||||||
|
if task.recurrence != 'none':
|
||||||
|
task.create_next_recurrence()
|
||||||
|
|
||||||
task.save()
|
task.save()
|
||||||
|
|
||||||
next_url = request.POST.get('next', 'dashboard')
|
next_url = request.POST.get('next', 'dashboard')
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Debug views for troubleshooting mobile app."""
|
||||||
|
|
||||||
|
from django.http import JsonResponse, HttpResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def debug_user_agent(request):
|
||||||
|
"""Return the User-Agent header for debugging mobile app."""
|
||||||
|
user_agent = request.META.get('HTTP_USER_AGENT', 'No User-Agent')
|
||||||
|
|
||||||
|
# Simple HTML response that's easy to see in iframe
|
||||||
|
html = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Debug Info</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: monospace; padding: 20px; }}
|
||||||
|
.detected {{ color: green; font-weight: bold; }}
|
||||||
|
.not-detected {{ color: red; font-weight: bold; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Mobile App Debug Info</h2>
|
||||||
|
<p><strong>User-Agent:</strong><br>{user_agent}</p>
|
||||||
|
<p><strong>Contains 'wv':</strong> {'wv' in user_agent.lower()}</p>
|
||||||
|
<p><strong>Contains 'CapacitorHttp':</strong> {'CapacitorHttp' in user_agent}</p>
|
||||||
|
<p><strong>Is Mobile App Detected:</strong>
|
||||||
|
<span class="{'detected' if ('wv' in user_agent.lower() or 'CapacitorHttp' in user_agent) else 'not-detected'}">
|
||||||
|
{('wv' in user_agent.lower() or 'CapacitorHttp' in user_agent)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return HttpResponse(html)
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="{% static 'favicons/favicon.svg' %}">
|
<link rel="icon" type="image/svg+xml" href="{% static 'favicons/favicon.svg' %}">
|
||||||
<link rel="alternate icon" href="{% static 'favicons/favicon.svg' %}" type="image/svg+xml">
|
<link rel="alternate icon" href="{% static 'favicons/favicon.svg' %}" type="image/svg+xml">
|
||||||
|
|
||||||
<link rel="stylesheet" href="{% static 'css/app.css' %}">
|
<link rel="stylesheet" href="{% static 'css/app.css' %}?v=6">
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user