Files
TimeTracker/app/templates/comments/_comment.html
T
Dries Peeters 8b6e61873b Use system date/time format by default with optional user override
Display formats for dates and times now follow the system settings (Admin
settings) by default. Users can override in their profile (User settings) or
choose "Use system default" so their view matches the rest of the system.

Backend:
- User.date_format and User.time_format are nullable; null means use system.
- Migration 120 makes these columns nullable (existing rows unchanged).
- get_resolved_date_format_key() and get_resolved_time_format_key() in
  timezone utils return the effective key (user or system) for templates and API.
- Context processor injects resolved_date_format_key and resolved_time_format_key
  so base.html and JS (window.userPrefs) always see the resolved format.
- User settings form: "Use system default" option and save logic for null.
- User.to_dict() includes resolved date_format, time_format, and timezone for
  API clients (e.g. mobile).

Web:
- base.html uses resolved keys for window.userPrefs (no hardcoded fallback).
- Replaced display-only strftime() in templates with |user_date, |user_datetime,
  |user_time, and |format_date so all visible dates/times respect settings.
  Left <input type="date"> values and URL/API params as YYYY-MM-DD where required.

Mobile:
- ApiClient.getCurrentUser() and user prefs provider load resolved prefs from
  /api/v1/users/me.
- date_format_utils maps API keys to intl patterns; formatDate, formatTime,
  formatDateTime, formatDateRange used for display.
- Time entries screen (filter dialog), time entry form, time entry card, and
  home dashboard use user prefs for formatting; API requests still send ISO dates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 08:20:08 +01:00

197 lines
9.7 KiB
HTML

{# Set default depth if not provided #}
{% set depth = depth | default(0) %}
{% set max_depth = 10 %}
{# Prevent infinite recursion by limiting depth #}
{% if depth < max_depth %}
<!-- Single comment template -->
<div class="comment" id="comment-{{ comment.id }}" data-comment-id="{{ comment.id }}">
<div class="comment-header d-flex align-items-center mb-2">
<div class="comment-avatar me-3">
<div class="avatar-circle">
{{ (comment.author.full_name or comment.author.username)[0].upper() }}
</div>
</div>
<div class="comment-meta flex-grow-1">
<div class="comment-author">
<strong>{{ comment.author.full_name or comment.author.username }}</strong>
{% if comment.author.is_admin %}
<span class="badge bg-primary ms-1">{{ _('Admin') }}</span>
{% endif %}
</div>
<div class="comment-timestamp text-muted">
<i class="fas fa-clock me-1"></i>
<time datetime="{{ comment.created_at.isoformat() }}" title="{{ comment.created_at|user_datetime }}">
{{ comment.created_at|user_datetime }}
</time>
{% if comment.created_at != comment.updated_at %}
<span class="text-muted ms-2" title="{{ _('Edited on') }} {{ comment.updated_at|user_datetime }}">
<i class="fas fa-edit"></i> {{ _('edited') }}
</span>
{% endif %}
</div>
</div>
<div class="comment-actions">
{% if comment.can_edit(current_user) %}
<button type="button" class="btn btn-sm btn-outline-secondary me-1" onclick="editComment({{ comment.id }})" title="{{ _('Edit') }}">
<i class="fas fa-edit"></i>
</button>
{% endif %}
{% if comment.can_delete(current_user) %}
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteComment({{ comment.id }})" title="{{ _('Delete') }}">
<i class="fas fa-trash"></i>
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-outline-primary" onclick="replyToComment({{ comment.id }})" title="{{ _('Reply') }}">
<i class="fas fa-reply"></i>
</button>
</div>
</div>
<div class="comment-content" id="comment-content-{{ comment.id }}">
<div class="comment-text">{{ comment.content | nl2br | safe }}</div>
<!-- Attachments -->
{% if comment.attachments and comment.attachments|length > 0 %}
<div class="comment-attachments mt-3 pt-3 border-top">
<div class="d-flex flex-wrap gap-2">
{% for attachment in comment.attachments %}
<div class="comment-attachment-item d-flex align-items-center gap-2 p-2 bg-light rounded border" style="max-width: 100%;">
<div class="flex-shrink-0">
{% if attachment.is_pdf %}
<i class="fas fa-file-pdf text-danger"></i>
{% elif attachment.is_image %}
<i class="fas fa-file-image text-primary"></i>
{% elif attachment.is_document %}
<i class="fas fa-file-word text-info"></i>
{% else %}
<i class="fas fa-file text-secondary"></i>
{% endif %}
</div>
<div class="flex-grow-1 min-w-0">
<a href="{{ url_for('comments.download_attachment', attachment_id=attachment.id) }}"
class="text-decoration-none text-primary d-block text-truncate"
title="{{ attachment.original_filename }}">
{{ attachment.original_filename }}
</a>
<small class="text-muted">{{ attachment.file_size_display }}</small>
</div>
{% if comment.can_edit(current_user) %}
<div class="flex-shrink-0">
<form method="POST" action="{{ url_for('comments.delete_attachment', attachment_id=attachment.id) }}"
class="d-inline"
onsubmit="return confirm('{{ _('Are you sure you want to delete this attachment?') }}');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-link text-danger p-0" title="{{ _('Delete') }}">
<i class="fas fa-times"></i>
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Add Attachment Button (only if user can edit) -->
{% if comment.can_edit(current_user) %}
<div class="comment-attachment-actions mt-2">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="toggleAttachmentForm({{ comment.id }})"
id="attach-btn-{{ comment.id }}">
<i class="fas fa-paperclip me-1"></i>{{ _('Add Attachment') }}
</button>
<!-- Attachment Upload Form (initially hidden) -->
<div class="attachment-upload-form d-none mt-2" id="attachment-form-{{ comment.id }}">
<form method="POST" action="{{ url_for('comments.upload_comment_attachment', comment_id=comment.id) }}"
enctype="multipart/form-data" class="d-flex gap-2 align-items-end">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="flex-grow-1">
<input type="file" name="file" class="form-control form-control-sm" required
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.xls,.xlsx,.zip,.rar">
<small class="text-muted">{{ _('Max 10 MB. Images, PDFs, documents, spreadsheets, archives') }}</small>
</div>
<button type="submit" class="btn btn-sm btn-primary">
<i class="fas fa-upload me-1"></i>{{ _('Upload') }}
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleAttachmentForm({{ comment.id }})">
{{ _('Cancel') }}
</button>
</form>
</div>
</div>
{% endif %}
</div>
<!-- Edit form (initially hidden) -->
<div class="comment-edit-form d-none" id="edit-form-{{ comment.id }}">
<form method="POST" action="{{ url_for('comments.edit_comment', comment_id=comment.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<textarea name="content" class="form-control" rows="3" required>{{ comment.content }}</textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-save me-1"></i>{{ _('Save') }}
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit({{ comment.id }})">
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
</button>
</div>
</form>
</div>
<!-- Reply form (initially hidden) -->
<div class="comment-reply-form d-none mt-3" id="reply-form-{{ comment.id }}">
<form method="POST" action="{{ url_for('comments.create_comment') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if comment.project_id %}
<input type="hidden" name="project_id" value="{{ comment.project_id }}">
{% else %}
<input type="hidden" name="task_id" value="{{ comment.task_id }}">
{% endif %}
<input type="hidden" name="parent_id" value="{{ comment.id }}">
<div class="mb-3">
<textarea name="content" class="form-control" rows="3" placeholder="{{ _('Write your reply...') }}" required></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-reply me-1"></i>{{ _('Reply') }}
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelReply({{ comment.id }})">
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
</button>
</div>
</form>
</div>
<!-- Replies -->
{% if comment.replies and depth < max_depth %}
<div class="comment-replies mt-3">
{% for reply in comment.replies %}
<div class="comment-reply ms-4">
{% with comment=reply, depth=depth+1 %}
{% include 'comments/_comment.html' %}
{% endwith %}
</div>
{% endfor %}
</div>
{% elif comment.replies and depth >= max_depth %}
<div class="comment-replies mt-3">
<div class="text-muted small">
<i class="fas fa-info-circle"></i> {{ _('Replies are too deeply nested to display.') }}
</div>
</div>
{% endif %}
</div>
{% else %}
{# Depth limit reached - show placeholder #}
<div class="comment" id="comment-{{ comment.id }}" data-comment-id="{{ comment.id }}">
<div class="text-muted small">
<i class="fas fa-info-circle"></i> {{ _('Comment nesting too deep to display.') }}
</div>
</div>
{% endif %}