Merge branch 'main' into django-5-2

This commit is contained in:
Klaas van Schelven
2025-09-05 09:49:15 +02:00
54 changed files with 1506 additions and 1390 deletions

View File

@@ -1,7 +1,4 @@
[bandit]
# Skip B108 ("hardcoded temp dir"), see https://github.com/bugsink/bugsink/issues/174
skips = B108
# Exclude any file named tests.py anywhere under the tree
exclude = tests.py

View File

@@ -1,5 +1,53 @@
# Changes
## 2.0.0 (...)
### Backwards incompatible changes
* Python 3.9 is no longer supported (12af5302efdd)
#### Non-root Docker
The provided Docker container no longer runs the Bugsink process as the root
user. This improves security (defense in depth), but may require changes to
your setup (i.e. volume permissions).
Setups that mount the `/data` dir as a volume must ensure that the directory is
owned by UID 14237 (the user the process runs as inside the container).
[Further migration
instructions](https://github.com/bugsink/bugsink/issues/176#issuecomment-3139184180)
If you have not mounted any volumes, you will not be visibly affected by this change.
#### Hardening of Temporary-Directory
Bugsink now requires ownership of the `INGEST_STORE_BASE_DIR` directory to avoid
certain classes of local privilege escalation attacks. (see #174)
If you manually configured this directory to be something that the process
running Bugsink _cannot_ own (e.g. to `/tmp/` without a further subdir), you
must change it to something it can own (e.g. the default of `/tmp/bugsink/ingestion`)
The Docker image is not affected by this (manual configuration wasn't possible to
begin with).
### Various Improvements & Fixes
* When selecting text in the stacktrace frameHeader, don't toggle the frame (d62d016be3aa)
* i18n support and Chinese translation (See #192, #161)
* minor changes to `send_json` util (f0d3667121ab, c38ca8c58a4c)
* Docker: bugsink-show-version on-start (42ba5a71facc)
* Implement `vacuum_ingest_dir` management command (See #163)
### Dependency updates
* Replace `python-sourcemap` with `ecma426` (see 0764024389fc)
* django 4.2 => django 5.2
* Tailwind 3 => Tailwind 4
* django-tailwind 3.6 => 4.2
* `inotify_simple` => 2.0
## 1.7.6 (1 August 2025)
* envelope-headers `sent_at` check should allow 00+00 (See #179)

View File

@@ -47,6 +47,13 @@ COPY bugsink/conf_templates/docker.py.template bugsink_conf.py
RUN apt update && apt install -y git
RUN pip install -e .
RUN groupadd --gid 14237 bugsink \
&& useradd --uid 14237 --gid bugsink \
&& mkdir -p /data \
&& chown -R bugsink:bugsink /data
USER bugsink
RUN ["bugsink-manage", "migrate", "snappea", "--database=snappea"]
HEALTHCHECK CMD python -c 'import requests; requests.get("http://localhost:8000/health/ready").raise_for_status()'

View File

@@ -72,6 +72,13 @@ RUN --mount=type=cache,target=/var/cache/buildkit/pip \
RUN cp /usr/local/lib/python3.12/site-packages/bugsink/conf_templates/docker.py.template /app/bugsink_conf.py && \
cp /usr/local/lib/python3.12/site-packages/bugsink/gunicorn.docker.conf.py /app/gunicorn.docker.conf.py
RUN groupadd --gid 14237 bugsink \
&& useradd --uid 14237 --gid bugsink \
&& mkdir -p /data \
&& chown -R bugsink:bugsink /data
USER bugsink
RUN ["bugsink-manage", "migrate", "snappea", "--database=snappea"]
HEALTHCHECK CMD python -c 'import requests; requests.get("http://localhost:8000/health/ready").raise_for_status()'

View File

@@ -16,6 +16,9 @@ Portions of this software are licensed as follows:
Please refer to the license of the respective component in your package
manager's repository.
* The following files are licensed under the Python Software Foundation License
* bsmain/future_python.py
* Content outside of the above mentioned directories or restrictions above is
available under the license as defined below:

52
bsmain/future_python.py Normal file
View File

@@ -0,0 +1,52 @@
# A backport of a not-yet-released version of Python's os.makedirs
#
# License: Python Software Foundation License
#
# From:
# https://github.com/python/cpython/pull/23901 as per
# https://github.com/python/cpython/pull/23901/commits/128ff8b46696c26e2cea5609cf9840b9425dcccf
#
# Note on stability: os.makedirs has not seen any changes after Python 3.7 up to
# 3.13 (3.14 is in pre-release, so unlikely to see changes). This means that the
# current code can be used as a "extra feature" drop in for at least those versions.
from os import path, mkdir, curdir
def makedirs(name, mode=0o777, exist_ok=False, *, recursive_mode=False):
"""makedirs(name [, mode=0o777][, exist_ok=False][, recursive_mode=False])
Super-mkdir; create a leaf directory and all intermediate ones. Works like
mkdir, except that any intermediate path segment (not just the rightmost)
will be created if it does not exist. If the target directory already
exists, raise an OSError if exist_ok is False. Otherwise no exception is
raised. If recursive_mode is True, the mode argument will affect the file
permission bits of any newly-created, intermediate-level directories. This
is recursive.
"""
head, tail = path.split(name)
if not tail:
head, tail = path.split(head)
if head and tail and not path.exists(head):
try:
if recursive_mode:
makedirs(head, mode=mode, exist_ok=exist_ok,
recursive_mode=True)
else:
makedirs(head, exist_ok=exist_ok)
except FileExistsError:
# Defeats race condition when another thread created the path
pass
cdir = curdir
if isinstance(tail, bytes):
cdir = bytes(curdir, 'ASCII')
if tail == cdir: # xxx/newdir/. exists if xxx/newdir exists
return
try:
mkdir(name, mode)
except OSError:
# Cannot rely on checking for EEXIST, since the operating system
# could give priority to other errors like EACCES or EROFS
if not exist_ok or not path.isdir(name):
raise

View File

@@ -25,7 +25,7 @@
<div class="ml-auto mt-6">
<form action="{% url "auth_token_create" %}" method="post">
{% csrf_token %} {# margins display slightly different from the <a href version that I have for e.g. project memembers, but I don't care _that_ much #}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Add Token" %}</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Add Token" %}</button>
</form>
</div>
</div>
@@ -51,7 +51,7 @@
<td class="p-4">
<div class="flex justify-end">
<button name="action" value="delete:{{ auth_token.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Delete" %}</button>
<button name="action" value="delete:{{ auth_token.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Delete" %}</button>
</div>
</td>

102
bsmain/utils.py Normal file
View File

@@ -0,0 +1,102 @@
import os
import stat
import logging
from .future_python import makedirs
PRIVATE_MODE = 0o700
GLOBALLY_WRITABLE_MASK = 0o002
logger = logging.getLogger("bugsink.security")
class B108SecurityError(Exception):
pass
def b108_makedirs(path):
"""
Create (or validate) an app working directory with B108-style hardening against local privilege escalation:
* Create without if-exists checks to avoid TOCTOU (makedirs(..., exist_ok=True)).
* Final directory invariants:
1. owned by the current uid
2. private mode (700)
* Path invariants (from the leaf up to the first root-owned ancestor, which is assumed to be secure):
1. every segment is owned by the current uid
2. no symlinks anywhere (somewhat redundant given "owned by us", but we're playing safe)
This removes the risk of being redirected into unintended locations (symlink/rename tricks) and of leaking data
into attacker-controlled files or directories.
### Backwards compatibility notes
On already running systems, directories may have been created with laxer permissions. We simply warn about those,
(rather than try to fix the problem) because in the general case we cannot determine where the "Bugsink boundary"
is (e.g. we wouldn't want to mess with $HOME's permissions, which is what would happen if we simply apply the "chmod
for current uid" rule all the way up).
### Further notes:
* Our model for file-based attack vectors is simply: inside the 700 dir, you'll be good no matter what. In other
words: no analogous checks at the file level.
* This function implements post-verification (i.e. "in theory it's too late"); since it operates at the dir-level we
believe "in practice it's in time" (you might trick us into writing a directory somewhere, but right after it'll
fail the check and no files will be written)
"""
makedirs(path, mode=PRIVATE_MODE, exist_ok=True, recursive_mode=True)
my_uid = os.getuid()
# the up-the-tree checks are unconditional (cheap enough, and they guard against scenarios in which an attacker
# previously created something in the way, so we can't skip because os.makedirs says "it already exists")
# separate from the "up-the-tree" loop b/c the target path may not be root.
st = os.lstat(path)
if st.st_uid != my_uid:
raise B108SecurityError(f"Target path owned by uid other than me: {path}")
if (st.st_mode & 0o777) != PRIVATE_MODE:
# NOTE: warn-only to facilitate a migration doesn't undo all our hardening for post-migration/fresh installs,
# because we still check self-ownership up to root.
logger.warning(
"SECURITY: Target path does not have private mode (700): %s has mode %03o", path, st.st_mode & 0o777)
current = path
while True:
st = os.lstat(current)
if st.st_uid == 0:
# we stop checking once we reach a root-owned dir; at some level you'll "hit the system boundary" which is
# secure by default (or it's compromised, in which case nothing helps us). We work on the assumption that
# this boundary is correctly setup, e.g. if it's /tmp it will have the sticky bit set.
break
if stat.S_ISLNK(st.st_mode):
raise B108SecurityError("Symlink in path at %s while creating %s" % (current, path))
# if not stat.S_ISDIR(st.st_mode): not needed, because os.makedirs would trigger a FileExistsError over that
if st.st_uid != my_uid:
# (avoiding tripping over root is implied by the `break` in the above)
raise B108SecurityError("Parent directory of %s not owned by my uid or root: %s" % (path, current))
if (current != path) and (st.st_mode & GLOBALLY_WRITABLE_MASK): # skipped for target (more strict check above)
# note: in practice this won't trigger for "plain migrations" i.e. ones where no manual changes were made,
# because the pre-existing code created with 0o755; still: it's a good check to have.
#
# note: we don't additionally check on group-writable because we don't want to make too many assumptions
# about group setup (e.g. user private groups are common on Linux)
logger.warning("SECURITY: Parent directory of target path %s is globally writeable: %s", path, current)
parent = os.path.dirname(current)
if parent == current: # reached root
# weird that this would not be root-owned (break above) but I'd rather not hang indefinitely for that.
break
current = parent

View File

@@ -66,7 +66,8 @@ DEFAULTS = {
"MAX_HEADER_SIZE": 8 * _KIBIBYTE,
# Locations of files & directories:
"INGEST_STORE_BASE_DIR": "/tmp/bugsink/ingestion",
# no_bandit_expl: the usage of this path (via get_filename_for_event_id) is protected with `b108_makedirs`
"INGEST_STORE_BASE_DIR": "/tmp/bugsink/ingestion", # nosec
"EVENT_STORAGES": {},
# Security:

View File

@@ -60,13 +60,16 @@ SNAPPEA = {
"NUM_WORKERS": int(os.getenv("SNAPPEA_NUM_WORKERS", 2)),
"STATS_RETENTION_MINUTES": int(os.getenv("SNAPPEA_STATS_RETENTION_MINUTES", 60 * 24 * 7)),
# in our Dockerfile the foreman is started exactly once (no check against collisions needed) and whaterver is
# in our Dockerfile the foreman is started exactly once (no check against collisions needed) and whatever is
# running the container is responsible for the container's lifecycle (again: no pid-file check needed to avoid
# collisions)
"PID_FILE": None,
}
# Not actually a "database", this is a (tmp to the container) message queue.
DATABASES["snappea"]["NAME"] = '/tmp/snappea.sqlite3'
if os.getenv("DATABASE_URL"):
DATABASE_URL = os.getenv("DATABASE_URL")
@@ -100,10 +103,6 @@ else:
# which allows for throwaway setups (no volume mounted) to work out of the box.
DATABASES['default']['NAME'] = os.getenv("DATABASE_PATH", '/data/db.sqlite3')
database_path = os.path.dirname(DATABASES['default']['NAME'])
if not os.path.exists(database_path):
print(f"WARNING: {database_path} dir does not exist; creating it.")
print("WARNING: data will be lost when the container is removed.")
os.makedirs(database_path)
if os.getenv("EMAIL_HOST"):

View File

@@ -45,7 +45,8 @@ SNAPPEA = {
"PID_FILE": None,
# alternatively (if you don't trust systemd to get at most one snappea foreman running; or if you don't use systemd
# at all), use the below:
# at all), use the below. Make sure the file is in a directory owned by the user running bugsink/snappea (the
# directory will be created if it does not exist yet).
# "PID_FILE": "{{ base_dir }}/snappea/snappea.pid",
"WAKEUP_CALLS_DIR": "{{ base_dir }}/snappea/wakeup",

View File

@@ -64,7 +64,9 @@ if not I_AM_RUNNING == "TEST":
SNAPPEA = {
"TASK_ALWAYS_EAGER": True, # at least for (unit) tests, this is a requirement
"NUM_WORKERS": 1,
"PID_FILE": "/tmp/snappea.pid", # for development: a thing to 'tune' to None to test the no-pid-check branches.
# no_bandit_expl: development setting, we know that this is insecure "in theory" at least
"PID_FILE": "/tmp/bugsink/snappea.pid", # nosec B108
}
EMAIL_HOST = os.getenv("EMAIL_HOST")

View File

@@ -2,7 +2,7 @@ from os.path import basename
from datetime import datetime, timezone
from uuid import UUID
import json
import sourcemap
import ecma426
from issues.utils import get_values
from bugsink.transaction import delay_on_commit
@@ -134,7 +134,7 @@ def apply_sourcemaps(event_data):
]
sourcemap_for_filename = {
filename: sourcemap.loads(_postgres_fix(meta.file.data))
filename: ecma426.loads(_postgres_fix(meta.file.data))
for (filename, meta) in filenames_with_metas
}
@@ -155,18 +155,18 @@ def apply_sourcemaps(event_data):
if frame.get("filename") in sourcemap_for_filename:
sm = sourcemap_for_filename[frame["filename"]]
token = sm.lookup(frame["lineno"] + FROM_DISPLAY, frame["colno"])
mapping = sm.lookup_left(frame["lineno"] + FROM_DISPLAY, frame["colno"])
if token.src in source_for_filename:
lines = source_for_filename[token.src]
if mapping.source in source_for_filename:
lines = source_for_filename[mapping.source]
frame["pre_context"] = lines[max(0, token.src_line - 5):token.src_line]
frame["context_line"] = lines[token.src_line]
frame["post_context"] = lines[token.src_line + 1:token.src_line + 5]
frame["lineno"] = token.src_line + TO_DISPLAY
frame['filename'] = token.src
frame['function'] = token.name
# frame["colno"] = token.src_col + TO_DISPLAY not actually used
frame["pre_context"] = lines[max(0, mapping.original_line - 5):mapping.original_line]
frame["context_line"] = lines[mapping.original_line]
frame["post_context"] = lines[mapping.original_line + 1:mapping.original_line + 5]
frame["lineno"] = mapping.original_line + TO_DISPLAY
frame['filename'] = mapping.source
frame['function'] = mapping.name
# frame["colno"] = mapping.original_column + TO_DISPLAY not actually used
elif frame.get("filename") in debug_id_for_filename:
# The event_data reports that a debug_id is available for this filename, but we don't have it; this

View File

@@ -0,0 +1,97 @@
import os
import re
import time
from django.core.management.base import BaseCommand, CommandError
from django.utils._os import safe_join
from bugsink.app_settings import get_settings
# Pattern for valid event IDs (32-character hex strings, lowercase, no dashes, as generated by
# `get_filename_for_event_id`
#
EVENT_ID_PATTERN = re.compile(r'^[0-9a-f]{32}$')
class Command(BaseCommand):
help = "Clean up old files from the ingest directory. Removes files older than specified days (default: 7)."
def add_arguments(self, parser):
parser.add_argument(
'--days',
type=int,
default=7,
help='Remove files older than this many days (default: 7)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be deleted without actually deleting'
)
def handle(self, *args, **options):
days = options['days']
dry_run = options['dry_run']
if days < 0:
raise CommandError("Days must be a non-negative number")
ingest_dir = get_settings().INGEST_STORE_BASE_DIR
if not os.path.exists(ingest_dir):
self.stdout.write("Ingest directory does not exist: {}".format(ingest_dir))
return
# Calculate cutoff time (files older than this will be removed)
cutoff_time = time.time() - (days * 24 * 60 * 60)
files_processed = 0
files_removed = 0
unexpected_files = []
for filename in os.listdir(ingest_dir):
filepath = safe_join(ingest_dir, filename)
# Skip directories
if os.path.isdir(filepath):
continue
files_processed += 1
# Check if filename matches expected event ID format
if not EVENT_ID_PATTERN.match(filename):
unexpected_files.append(filename)
continue
# Check file age
try:
file_mtime = os.path.getmtime(filepath)
if file_mtime < cutoff_time:
age_days = (time.time() - file_mtime) / (24 * 60 * 60)
if dry_run:
self.stdout.write("Would remove: {} (age: {:.1f} days)".format(filename, age_days))
else:
os.remove(filepath)
self.stdout.write("Removed: {} (age: {:.1f} days)".format(filename, age_days))
files_removed += 1
except OSError as e:
self.stderr.write("Error processing {}: {}".format(filename, e))
# Report results
if dry_run:
self.stdout.write("\nDry run completed:")
else:
self.stdout.write("\nCleanup completed:")
self.stdout.write(" Files processed: {}".format(files_processed))
self.stdout.write(" Files {}: {}".format('would be removed' if dry_run else 'removed', files_removed))
if unexpected_files:
self.stdout.write(" Unexpected files found (skipped): {}".format(len(unexpected_files)))
for filename in unexpected_files:
self.stdout.write(" - {}".format(filename))
self.stdout.write(
" Warning: Found files that don't match expected event ID format. "
"These files were not touched to avoid accidental deletion."
)

View File

@@ -38,6 +38,7 @@ from releases.models import create_release_if_needed
from alerts.tasks import send_new_issue_alert, send_regression_alert
from compat.timestamp import format_timestamp, parse_timestamp
from tags.models import digest_tags
from bsmain.utils import b108_makedirs
from .parsers import StreamingEnvelopeParser, ParseError
from .filestore import get_filename_for_event_id
@@ -633,7 +634,7 @@ class IngestEnvelopeAPIView(BaseIngestAPIView):
raise ParseError("event_id in envelope headers is not a valid UUID")
filename = get_filename_for_event_id(envelope_headers["event_id"])
os.makedirs(os.path.dirname(filename), exist_ok=True)
b108_makedirs(os.path.dirname(filename))
return MaxDataWriter("MAX_EVENT_SIZE", open(filename, 'wb'))
# everything else can be discarded; (we don't check for individual size limits, because these differ

View File

@@ -2,11 +2,11 @@
{% load i18n %}
<form action="{% url this_view issue_pk=issue.pk nav="last" %}" method="get">{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #}
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'search...' %}" class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'search...' %}" class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md mr-2"/>
</form>
{% if has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
<a href="{% url this_view issue_pk=issue.pk nav="first" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="First event">
<a href="{% url this_view issue_pk=issue.pk nav="first" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="First event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -16,7 +16,7 @@
{% endif %}
{% if has_prev %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="prev" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Previous event">
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="prev" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Previous event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -26,7 +26,7 @@
{% endif %}
{% if has_next %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="next" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Next event">
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="next" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Next event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -36,7 +36,7 @@
{% endif %}
{% if has_next %}
<a href="{% url this_view issue_pk=issue.pk nav="last" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Last event">
<a href="{% url this_view issue_pk=issue.pk nav="last" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Last event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}

View File

@@ -16,7 +16,7 @@
{% csrf_token %}
{% if issue.is_resolved %}{# i.e. buttons disabled #}
{# see issues/tests.py for why this is turned off ATM #}
{# <button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="reopen">Reopen</button> #}
{# <button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" name="action" value="reopen">Reopen</button> #}
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if issue.project.has_releases %}
@@ -34,16 +34,16 @@
{% if issue.project.has_releases %}
{# 'by next' is shown even if 'by current' is also shown: just because you haven't seen 'by current' doesn't mean it's actually already solved; and in fact we show this option first precisely because we can always show it #}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
<div class="dropdown">
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-800 dark:text-slate-100 fill-slate-800 dark:fill-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-800 dark:text-slate-100 fill-slate-800 dark:fill-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
{# note that we can depend on get_latest_release being available, because we're in the 'project.has_releases' branch #}
<div class="dropdown-content-right flex-col pl-2">
{% if not issue.occurs_in_last_release %}
<button name="action" value="resolved_release:{{ issue.project.get_latest_release.version }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring text-left whitespace-nowrap">Resolved in latest ({{ issue.project.get_latest_release.get_short_version }})</button>
<button name="action" value="resolved_release:{{ issue.project.get_latest_release.version }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 text-left whitespace-nowrap">Resolved in latest ({{ issue.project.get_latest_release.get_short_version }})</button>
{% else %}
<button name="action" value="resolved_release:{{ issue.project.get_latest_release.version }}" disabled class="block self-stretch font-bold text-slate-300 dark:text-slate-600 border-slate-200 dark:border-slate-700 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 text-left whitespace-nowrap">Resolved in latest ({{ issue.project.get_latest_release.get_short_version }})</button>
{% endif %}
@@ -53,7 +53,7 @@
{% else %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
{% endif %}
{% endspaceless %}
@@ -61,18 +61,18 @@
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if not issue.is_muted and not issue.is_resolved %}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
{% endif %}
<div class="dropdown">
{% if not issue.is_muted and not issue.is_resolved %}
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<div class="dropdown-content-right flex-col">
{% for mute_option in mute_options %}
<button name="action" value="mute_{{ mute_option.for_or_until }}:{{ mute_option.period_name }},{{ mute_option.nr_of_periods }},{{ mute_option.gte_threshold }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring text-left whitespace-nowrap">{% if mute_option.for_or_until == "for" %}{{ mute_option.nr_of_periods }} {{ mute_option.period_name }}{% if mute_option.nr_of_periods != 1 %}s{% endif %}{% else %}{{ mute_option.gte_threshold }} events per {% if mute_option.nr_of_periods != 1%} {{ mute_option.nr_of_periods }} {{ mute_option.period_name }}s{% else %} {{ mute_option.period_name }}{% endif %}{% endif %}</button>
<button name="action" value="mute_{{ mute_option.for_or_until }}:{{ mute_option.period_name }},{{ mute_option.nr_of_periods }},{{ mute_option.gte_threshold }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 text-left whitespace-nowrap">{% if mute_option.for_or_until == "for" %}{{ mute_option.nr_of_periods }} {{ mute_option.period_name }}{% if mute_option.nr_of_periods != 1 %}s{% endif %}{% else %}{{ mute_option.gte_threshold }} events per {% if mute_option.nr_of_periods != 1%} {{ mute_option.nr_of_periods }} {{ mute_option.period_name }}s{% else %} {{ mute_option.period_name }}{% endif %}{% endif %}</button>
{% endfor %}
</div>
{% else %}
@@ -85,7 +85,7 @@
</div>
{% if issue.is_muted and not issue.is_resolved %}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% endif %}

View File

@@ -16,11 +16,11 @@
<div class="flex place-content-end">
{# copy/paste of _event_nav, but not based on any event (we have none), prev/next are meaningless also; first/last only when we have an event_qs to navigate through #}
<form action="{% url this_view issue_pk=issue.pk nav="last" %}" method="get">{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #}
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md mr-2"/>
</form>
{% if event_qs_count %}
<a href="{% url this_view issue_pk=issue.pk nav="first" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="First event">
<a href="{% url this_view issue_pk=issue.pk nav="first" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="First event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" />
</svg>
</a>
@@ -39,7 +39,7 @@
</div>
{% if event_qs_count %}
<a href="{% url this_view issue_pk=issue.pk nav="last" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Last event">
<a href="{% url this_view issue_pk=issue.pk nav="last" %}{% current_qs %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Last event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}

View File

@@ -27,11 +27,11 @@
{# adapted copy/pasta from _event_nav #}
<div class="flex place-content-end">
<form action="." method="get">
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md mr-2"/>
</form>
{% if page_obj.has_previous %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
<a href="?{% add_to_qs page=1 %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="First page">
<a href="?{% add_to_qs page=1 %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="First page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" /></svg></a>
{% else %}
<div class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center" title="First page">
@@ -40,7 +40,7 @@
{% endif %}
{% if page_obj.has_previous %}
<a href="?{% add_to_qs page=page_obj.previous_page_number %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Previous page">
<a href="?{% add_to_qs page=page_obj.previous_page_number %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Previous page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -50,7 +50,7 @@
{% endif %}
{% if page_obj.has_next %}
<a href="?{% add_to_qs page=page_obj.next_page_number %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Next page">
<a href="?{% add_to_qs page=page_obj.next_page_number %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Next page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -60,7 +60,7 @@
{% endif %}
{% if page_obj.has_next %}
<a href="?{% add_to_qs page=page_obj.paginator.num_pages %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring inline-flex items-center justify-center" title="Last page">
<a href="?{% add_to_qs page=page_obj.paginator.num_pages %}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 inline-flex items-center justify-center" title="Last page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}

View File

@@ -32,8 +32,8 @@
<div class="mt-4">
<form action="{% url "history_comment_new" issue_pk=issue.id %}" method="post">
{% csrf_token %}
<textarea name="comment" placeholder="{% translate 'comments...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)"></textarea>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Post comment" %}</button>
<textarea name="comment" placeholder="{% translate 'comments...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)"></textarea>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3">{% translate "Post comment" %}</button>
</form>
</div>
</div>{# 'body' part of the balloon #}
@@ -98,8 +98,8 @@
<div class="js-comment-editable hidden">
<form action="{% url "history_comment_edit" issue_pk=issue.id comment_pk=turningpoint.pk %}" method="post">
{% csrf_token %}
<textarea name="comment" placeholder="comments..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)">{{ turningpoint.comment }}</textarea>{# note: we don't actually use {{ form.comments }} here; this means the show-red-on-invalid loop won't work but since everything is valid and we haven't implemented the other parts of that loop that's fine #}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Update comment</button>
<textarea name="comment" placeholder="comments..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)">{{ turningpoint.comment }}</textarea>{# note: we don't actually use {{ form.comments }} here; this means the show-red-on-invalid loop won't work but since everything is valid and we haven't implemented the other parts of that loop that's fine #}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3">Update comment</button>
</form>
</div>
{% endif %}

View File

@@ -9,7 +9,7 @@
{% block content %}
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600/50 dark:bg-slate-900/50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
<div class="text-center m-4">
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">{% translate "Delete Issues" %}</h3>
@@ -20,7 +20,7 @@
</div>
<div class="flex items-center justify-center space-x-4 mb-4">
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Cancel" %}</button>
<button id="confirmDelete" type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">{% translate "Delete" %}</button>
<button id="confirmDelete" type="submit" class="font-bold py-2 px-4 rounded-sm bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring-3">{% translate "Delete" %}</button>
</div>
</div>
</div>
@@ -46,7 +46,7 @@
</div>
<div class="ml-auto p-2">
<form action="." method="get">
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'Search issues...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'Search issues...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md"/>
</form>
</div>
</div>
@@ -64,7 +64,7 @@
<div class="m-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer" onclick="toggleContainedCheckbox(this); matchIssueCheckboxesStateToMain(this)">
{# the below sounds expensive, but this list is cached #}
{% if page_obj.object_list|length > 0 %}<input type="checkbox" class="bg-white dark:bg-slate-900 border-cyan-800 dark:border-cyan-400 text-cyan-500 dark:text-cyan-300 focus:ring-cyan-200 dark:focus:ring-cyan-700 m-4 cursor-pointer js-main-checkbox" onclick="event.stopPropagation(); matchIssueCheckboxesStateToMain(this.parentNode)"/>{% endif %}
{% if page_obj.object_list|length > 0 %}<input type="checkbox" class="bg-white dark:bg-slate-900 border-cyan-800 dark:border-cyan-400 text-cyan-500 dark:text-cyan-300 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 m-4 cursor-pointer js-main-checkbox" onclick="event.stopPropagation(); matchIssueCheckboxesStateToMain(this.parentNode)"/>{% endif %}
</div>
</td>
<td class="w-full ml-0 pb-4 pt-4 pr-4 flex">
@@ -72,7 +72,7 @@
{% if disable_resolve_buttons %}
{# see issues/tests.py for why this is turned off ATM #}
{# <button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="reopen">Reopen</button> #}
{# <button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" name="action" value="reopen">Reopen</button> #}
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if project.has_releases %}
@@ -90,23 +90,23 @@
{% if project.has_releases %}
{# 'by next' is shown even if 'by current' is also shown: just because you haven't seen 'by current' doesn't mean it's actually already solved; and in fact we show this option first precisely because we can always show it #}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
<div class="dropdown">
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-800 dark:text-slate-100 fill-slate-800 dark:fill-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-800 dark:text-slate-100 fill-slate-800 dark:fill-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
{# note that we can depend on get_latest_release being available, because we're in the 'project.has_releases' branch #}
<div class="dropdown-content-right flex-col pl-2">
{# note that an if-statement ("issue.occurs_in_last_release") is missing here, because we're not on the level of a single issue #}
{# handling of that question (but per-issue, and after-click) is done in views.py, _q_for_invalid_for_action() #}
<button name="action" value="resolved_release:{{ project.get_latest_release.version }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring text-left whitespace-nowrap">Resolved in latest ({{ project.get_latest_release.get_short_version }})</button>
<button name="action" value="resolved_release:{{ project.get_latest_release.version }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 text-left whitespace-nowrap">Resolved in latest ({{ project.get_latest_release.get_short_version }})</button>
</div>
</div>
{% else %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
{% endif %}
{% endspaceless %}
@@ -114,18 +114,18 @@
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if not disable_mute_buttons %}
<button name="action" value="mute" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md">{% translate "Mute" %}</button>
<button name="action" value="mute" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-s-md">{% translate "Mute" %}</button>
{% else %}
<button disabled name="action" value="mute" class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md">{% translate "Mute" %}</button>
{% endif %}
<div class="dropdown">
{% if not disable_mute_buttons %}
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<div class="dropdown-content-right flex-col">
{% for mute_option in mute_options %}
<button name="action" value="mute_{{ mute_option.for_or_until }}:{{ mute_option.period_name }},{{ mute_option.nr_of_periods }},{{ mute_option.gte_threshold }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring text-left whitespace-nowrap">{% if mute_option.for_or_until == "for" %}{{ mute_option.nr_of_periods }} {{ mute_option.period_name }}{% if mute_option.nr_of_periods != 1 %}s{% endif %}{% else %}{{ mute_option.gte_threshold }} events per {% if mute_option.nr_of_periods != 1%} {{ mute_option.nr_of_periods }} {{ mute_option.period_name }}s{% else %} {{ mute_option.period_name }}{% endif %}{% endif %}</button>
<button name="action" value="mute_{{ mute_option.for_or_until }}:{{ mute_option.period_name }},{{ mute_option.nr_of_periods }},{{ mute_option.gte_threshold }}" class="block self-stretch font-bold text-slate-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 text-left whitespace-nowrap">{% if mute_option.for_or_until == "for" %}{{ mute_option.nr_of_periods }} {{ mute_option.period_name }}{% if mute_option.nr_of_periods != 1 %}s{% endif %}{% else %}{{ mute_option.gte_threshold }} events per {% if mute_option.nr_of_periods != 1%} {{ mute_option.nr_of_periods }} {{ mute_option.period_name }}s{% else %} {{ mute_option.period_name }}{% endif %}{% endif %}</button>
{% endfor %}
</div>
{% else %}
@@ -135,16 +135,16 @@
</div>
{% if not disable_unmute_buttons %}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% endif %}
<div class="dropdown">
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 fill-slate-500 border-slate-300 ml-2 pl-4 pr-4 pb-2 pt-2 border-2 hover:bg-slate-200 active:ring rounded-md">...</button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 fill-slate-500 border-slate-300 ml-2 pl-4 pr-4 pb-2 pt-2 border-2 hover:bg-slate-200 active:ring-3 rounded-md">...</button>
<div class="dropdown-content-right flex-col">
<button type="button" onclick="showDeleteConfirmation()" class="block self-stretch font-bold text-red-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-red-50 dark:hover:bg-red-800 active:ring text-left whitespace-nowrap">{% translate "Delete" %}</button>
<button type="button" onclick="showDeleteConfirmation()" class="block self-stretch font-bold text-red-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-red-50 dark:hover:bg-red-800 active:ring-3 text-left whitespace-nowrap">{% translate "Delete" %}</button>
</div>
</div>
@@ -153,7 +153,7 @@
{# NOTE: "reopen" is not available in the UI as per the notes in issue_detail #}
{# only for resolved/muted items <button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Reopen" %}</button> #}
{# only for resolved/muted items <button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">{% translate "Reopen" %}</button> #}
</div>
</td>
@@ -165,7 +165,7 @@
<tr class="bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600 border-2 ">
<td>
<div class="m-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer" onclick="toggleContainedCheckbox(this); matchMainCheckboxStateToIssueCheckboxes()">
<input type="checkbox" {% if issue.id in unapplied_issue_ids %}checked{% endif %} name="issue_ids[]" value="{{ issue.id }}" class="bg-white dark:bg-slate-900 border-cyan-800 dark:border-cyan-400 text-cyan-500 dark:text-cyan-300 focus:ring-cyan-200 dark:focus:ring-cyan-700 m-4 cursor-pointer js-issue-checkbox" onclick="event.stopPropagation(); {# prevent the container's handler from undoing the default action #} matchMainCheckboxStateToIssueCheckboxes()"/>
<input type="checkbox" {% if issue.id in unapplied_issue_ids %}checked{% endif %} name="issue_ids[]" value="{{ issue.id }}" class="bg-white dark:bg-slate-900 border-cyan-800 dark:border-cyan-400 text-cyan-500 dark:text-cyan-300 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 m-4 cursor-pointer js-issue-checkbox" onclick="event.stopPropagation(); {# prevent the container's handler from undoing the default action #} matchMainCheckboxStateToIssueCheckboxes()"/>
</div>
</td>
<td class="w-full ml-0 pb-4 pt-4 pr-4">

View File

@@ -41,10 +41,10 @@
{% if forloop.counter0 == 0 %}
<div class="ml-auto flex flex-none flex-col-reverse 3xl:flex-row"> {# container of 2 divs: one for buttons, one for event-nav; on smaller screens these are 2 rows; on bigger they are side-by-side #}
<div class="flex place-content-end self-stretch pt-2 3xl:pt-0 {# <= to keep the buttons apart #} pb-4 lg:pb-0 {# <= to keep the buttons & h1-block apart #}">
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring" onclick="showAllFrames()">Show all</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring" onclick="showInAppFrames()">Show in-app</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring" onclick="showRaisingFrame()">Show raise</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring" onclick="hideAllFrames()">Collapse all</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3" onclick="showAllFrames()">Show all</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3" onclick="showInAppFrames()">Show in-app</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3" onclick="showRaisingFrame()">Show raise</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3" onclick="hideAllFrames()">Collapse all</button>
</div>
<div class="flex place-content-end">

View File

@@ -25,7 +25,7 @@
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} · {% translate "Alerts" %}</h1>
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_messaging_service_add" project_pk=project.pk %}">{% translate "Add" %}</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" href="{% url "project_messaging_service_add" project_pk=project.pk %}">{% translate "Add" %}</a>
</div>
</div>
@@ -65,8 +65,8 @@
<td class="p-4">
<div class="flex justify-end">
<button name="action" value="test:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Test" context "Alerts" %}</button>
<button name="action" value="remove:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" context "Alerts" %}</button>
<button name="action" value="test:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Test" context "Alerts" %}</button>
<button name="action" value="remove:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Remove" context "Alerts" %}</button>
</div>
</td>

View File

@@ -8,7 +8,7 @@
{% block content %}
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600/50 dark:bg-slate-900/50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
<div class="text-center m-4">
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">{% translate "Delete Project" %}</h3>
@@ -22,7 +22,7 @@
<form method="post" action="." id="deleteForm">
{% csrf_token %}
<input type="hidden" name="action" value="delete">
<button type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">Confirm</button>
<button type="submit" class="font-bold py-2 px-4 rounded-sm bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring-3">Confirm</button>
</form>
</div>
</div>
@@ -48,9 +48,9 @@
{% tailwind_formfield form.dsn %}
<div class="flex items-center mt-4">
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
<button type="button" id="deleteButton" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring ml-4 ml-auto">{% translate "Delete Project" %}</button>
<button type="button" id="deleteButton" class="font-bold py-2 px-4 rounded-sm bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring-3 ml-4 ml-auto">{% translate "Delete Project" %}</button>
</div>
</form>
</div>

View File

@@ -18,7 +18,7 @@
<div class="ml-auto"><!-- top, RHS (buttons) -->
{% if can_create %}
<div>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'project_new' %}">{% translate "New Project" %}</a>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" href="{% url 'project_new' %}">{% translate "New Project" %}</a>
</div>
{% endif %}
</div> {# top, RHS (buttons) #}
@@ -47,7 +47,7 @@
</div>
{% comment %}
<div class="ml-auto p-2">
<input type="text" name="search" placeholder="search projects..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
<input type="text" name="search" placeholder="search projects..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md"/>
</div>
{% endcomment %}
</div>
@@ -155,13 +155,13 @@
</div>
{% else %}
<div>
<button name="action" value="leave:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
<button name="action" value="leave:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
</div>
{% endif %}
{% else %}
{% if ownership_filter == "teams" or project.is_joinable or request.user.is_superuser %}{# ownership_filter check: you can always join your own team's projects, so if you're looking at a list of them... #}
<div>
<button name="action" value="join:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 dark:hover:bg-slate-700 active:ring rounded-md whitespace-nowrap">{% translate "Join" %}</button>
<button name="action" value="join:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 dark:hover:bg-slate-700 active:ring-3 rounded-md whitespace-nowrap">{% translate "Join" %}</button>
</div>
{% endif %}
{% endif %}

View File

@@ -38,7 +38,7 @@
{% tailwind_formfield form.role %}
{% tailwind_formfield form.send_email_alerts %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
{% if this_is_you %}
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
{% else %}

View File

@@ -25,7 +25,7 @@
<h1 class="text-4xl mt-4 font-bold">{% translate "Project Members" %}</h1>
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_members_invite" project_pk=project.pk %}">{% translate "Invite Member" %}</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" href="{% url "project_members_invite" project_pk=project.pk %}">{% translate "Invite Member" %}</a>
</div>
</div>
@@ -56,12 +56,12 @@
<td class="p-4">
<div class="flex justify-end">
{% if not member.accepted %}
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
{% endif %}
{% if request.user == member.user %}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
{% else %} {# NOTE: in our setup request_user_is_admin is implied because only admins may view the membership page #}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
{% endif %}
</div>
</td>

View File

@@ -22,8 +22,8 @@
{% blocktranslate with project_name=project.name role=membership.get_role_display %}You have been invited to join the project "{{ project_name }}" in the role of "{{ role }}". Please confirm by clicking the button below.{% endblocktranslate %}
</div>
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Accept" %}</button>
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Decline" %}</button>
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Accept" %}</button>
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">{% translate "Decline" %}</button>
</form>
</div>

View File

@@ -47,8 +47,8 @@
{% endif %}
</div>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Invite Member" %}</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Invite and add another" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Invite Member" %}</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">{% translate "Invite and add another" %}</button>
<a href="{% url "project_members" project_pk=project.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">{% translate "Cancel" %}</a>
</form>

View File

@@ -45,7 +45,7 @@
{% if service_config.last_failure_response_text %}
<div class="mt-2">
<p><strong>Response{% if service_config.last_failure_is_json %} (JSON){% endif %}:</strong></p>
<pre class="p-2 rounded text-sm mt-1 overflow-x-auto">{{ service_config.last_failure_response_text }}</pre>
<pre class="p-2 rounded-sm text-sm mt-1 overflow-x-auto">{{ service_config.last_failure_response_text }}</pre>
</div>
{% endif %}
</div>
@@ -60,7 +60,7 @@
{% tailwind_formfield field %}
{% endfor %}
<button name="action" value="add" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<button name="action" value="add" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">Save</button>
<a href="{% url "project_alerts_setup" project_pk=project.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">Cancel</a>
</form>

View File

@@ -23,7 +23,7 @@
{% tailwind_formfield form.visibility %}
{% tailwind_formfield form.retention_max_event_count %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</form>

View File

@@ -1,7 +1,7 @@
gunicorn==23.0.*
Django==5.2.*
sentry-sdk==2.*
django-tailwind==3.6.*
django-tailwind==4.2.*
jsonschema==4.25.*
semver==3.0.*
django-admin-autocomplete-filter==0.7.*
@@ -15,4 +15,4 @@ monofy==1.1.*
user-agents==2.2.*
fastjsonschema==2.21.*
verbose_csrf_middleware==1.0.*
sourcemap==0.2.*
ecma426>=0.2.0

View File

@@ -19,6 +19,7 @@ from django.utils._os import safe_join
from sentry_sdk_extensions import capture_or_log_exception
from performance.context_managers import time_to_logger
from bugsink.transaction import durable_atomic, get_stat
from bsmain.utils import b108_makedirs
from . import registry
from .models import Task
@@ -88,8 +89,7 @@ class Foreman:
signal.signal(signal.SIGTERM, self.handle_signal)
# We use inotify to wake up the Foreman when a new Task is created.
if not os.path.exists(self.settings.WAKEUP_CALLS_DIR):
os.makedirs(self.settings.WAKEUP_CALLS_DIR, exist_ok=True)
b108_makedirs(self.settings.WAKEUP_CALLS_DIR)
self.wakeup_calls = INotify()
self.wakeup_calls.add_watch(self.settings.WAKEUP_CALLS_DIR, flags.CREATE)
@@ -145,6 +145,7 @@ class Foreman:
# as per the above: not bullet proof, and non-critical, hence also: not a reason to crash on this.
logger.error("Startup: Ignored Error while checking PID file", exc_info=e)
# Note: no b108_makedirs here yet, because we can't assume a self-owned containing directory (see #195)
os.makedirs(os.path.dirname(self.settings.PID_FILE), exist_ok=True)
with open(self.settings.PID_FILE, "w") as f:
f.write(str(pid))

View File

@@ -2,6 +2,7 @@ import os
from django.db import models
from django.utils._os import safe_join
from bsmain.utils import b108_makedirs
from .settings import get_settings
from . import thread_uuid
@@ -47,8 +48,7 @@ class Stat(models.Model):
def wakeup_server():
wakeup_file = safe_join(get_settings().WAKEUP_CALLS_DIR, thread_uuid)
if not os.path.exists(get_settings().WAKEUP_CALLS_DIR):
os.makedirs(get_settings().WAKEUP_CALLS_DIR, exist_ok=True)
b108_makedirs(get_settings().WAKEUP_CALLS_DIR)
if not os.path.exists(wakeup_file):
with open(wakeup_file, "w"):

View File

@@ -4,8 +4,11 @@ from django.conf import settings
DEFAULTS = {
"TASK_ALWAYS_EAGER": False,
"PID_FILE": "/tmp/snappea.pid",
"WAKEUP_CALLS_DIR": "/tmp/snappea.wakeup",
# no_bandit_expl: monitored in #195
"PID_FILE": "/tmp/bugsink/snappea.pid", # nosec
# no_bandit_expl: the usage of this path (in the foreman) is protected with `b108_makedirs`
"WAKEUP_CALLS_DIR": "/tmp/snappea.wakeup", # nosec
"NUM_WORKERS": 4,

View File

@@ -88,6 +88,17 @@ function expandSection(element) {
}
function toggleFrameVisibility(frameHeader) {
console.log("toggling frame visibility");
const selection = window.getSelection().toString();
if (selection.length > 0) {
// don't toggle if the user is selecting text (which one might do to copy a
// filename or similar); the assumption here is: if there's a selection,
// it was just created using the mouse because non-selecting mouseclick
// deselect all text before the click is triggered. this assumption
// holds on desktop browsers; on mobile we might need to refine this.
return;
}
const frameDetails = frameHeader.parentNode.querySelector(".js-frame-details");
if (frameDetails.getAttribute("data-collapsed") === "true") {
expandSection(frameDetails);

View File

@@ -26,7 +26,7 @@
{% tailwind_formfield form.visibility %}
<div class="flex items-center mt-4">
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</div>
</form>

View File

@@ -18,7 +18,7 @@
<div class="ml-auto"><!-- top, RHS (buttons) -->
{% if can_create %}
<div>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'team_new' %}">{% translate "New Team" %}</a>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" href="{% url 'team_new' %}">{% translate "New Team" %}</a>
</div>
{% endif %}
</div> {# top, RHS (buttons) #}
@@ -34,7 +34,7 @@
</div>
{% comment %}
<div class="ml-auto p-2">
<input type="text" name="search" placeholder="search teams..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
<input type="text" name="search" placeholder="search teams..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-3-cyan-200 dark:focus:ring-3-cyan-700 rounded-md"/>
</div>
{% endcomment %}
</div>
@@ -105,13 +105,13 @@
</div>
{% else %}
<div>
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
</div>
{% endif %}
{% else %}
{% if team.is_joinable or request.user.is_superuser %}
<div>
<button name="action" value="join:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Join" %}</button>
<button name="action" value="join:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Join" %}</button>
</div>
{% endif %}
{% endif %}

View File

@@ -38,7 +38,7 @@
{% tailwind_formfield form.role %}
{% tailwind_formfield form.send_email_alerts %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
{% if this_is_you %}
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
{% else %}

View File

@@ -25,7 +25,7 @@
<h1 class="text-4xl mt-4 font-bold">{% translate "Team Members" %}</h1>
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">{% translate "Invite Member" %}</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">{% translate "Invite Member" %}</a>
</div>
</div>
@@ -56,12 +56,12 @@
<td class="p-4">
<div class="flex justify-end">
{% if not member.accepted %}
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
{% endif %}
{% if request.user == member.user %}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
{% else %} {# NOTE: in our setup request_user_is_admin is implied because only admins may view the membership page #}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
{% endif %}
</div>
</td>

View File

@@ -21,8 +21,8 @@
You have been invited to join the team "{{ team.name }}" in the role of "{{ membership.get_role_display }}". Please confirm by clicking the button below.
</div>
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Accept</button>
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Decline</button>
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">Accept</button>
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">Decline</button>
</form>
</div>

View File

@@ -49,8 +49,8 @@
{% endif %}
</div>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Invite Member" %}</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Invite and add another" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Invite Member" %}</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">{% translate "Invite and add another" %}</button>
<a href="{% url "team_members" team_pk=team.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">{% translate "Cancel" %}</a>
</form>

View File

@@ -21,7 +21,7 @@
{% tailwind_formfield form.name %}
{% tailwind_formfield form.visibility %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</form>

View File

@@ -25,7 +25,7 @@
<h2 class="text-2xl font-bold mt-4">POST Data</h2>
<form action="{% url 'csrf_debug' %}" method="post">
{% csrf_token %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Click to debug</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">Click to debug</button>
</form>
{% else %}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,26 @@
{
"name": "theme",
"version": "3.6.0",
"version": "4.2.0",
"description": "",
"scripts": {
"start": "npm run dev",
"build": "npm run build:clean && npm run build:tailwind",
"build:clean": "rimraf ../static/css/dist",
"build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify",
"dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w",
"tailwindcss": "node ./node_modules/tailwindcss/lib/cli.js"
"build:tailwind": "cross-env NODE_ENV=production postcss ./src/styles.css -o ../static/css/dist/styles.css --minify",
"dev": "cross-env NODE_ENV=development postcss ./src/styles.css -o ../static/css/dist/styles.css --watch"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.9",
"@tailwindcss/postcss": "^4.1.11",
"cross-env": "^7.0.3",
"postcss": "^8.4.24",
"postcss-import": "^15.1.0",
"postcss-nested": "^6.0.1",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"postcss-nested": "^7.0.2",
"postcss-simple-vars": "^7.0.1",
"rimraf": "^5.0.1",
"tailwindcss": "^3.3.2"
"rimraf": "^6.0.1",
"tailwindcss": "^4.1.11"
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
"postcss-import": {},
"@tailwindcss/postcss": {},
"postcss-simple-vars": {},
"postcss-nested": {}
},

View File

@@ -1,6 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.js";
@import "tailwindcss";
/* ibm-plex-sans-regular - cyrillic_cyrillic-ext_greek_latin_latin-ext_vietnamese */
@font-face {

View File

@@ -75,7 +75,6 @@ module.exports = {
*/
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp'),
require('@tailwindcss/aspect-ratio'),
],
}

View File

@@ -1,37 +0,0 @@
from django import template
from django.utils import timezone
register = template.Library()
# PAUSED FOR NOW
# reason: can we ever make better guesses (in general) than just providing iso formats?
# I'm thinking: no
# also: space-wise, there's always a window where you want all info (perhaps maybe the year). Namely: a couple of days
# ago,
#
# Further thoughts:
# you can also _decrease_ specificity when going back in time, e.g. "november 2022" or "Tuesday"
@register.filter # expects_localtime=True) ??
def short_given_now(value):
"""
[ ] format dates as ISO-like (or with short month notation),
never showing more than necessary.
chunks for consideration will be:
time-only: when it's on the current day.
or even: when it's on the current day or no more than a couple of hours ago.
day and month: not on current day, but no more than (365 minus small number) ago ... or the same: boundary or a few months
idea: if it's almost a full year ago the disambiguation will be helpful, and this also helps against confusion
de hint met daarin de volledige datum als default dinges?
"""
# take a look at how the standard Django filters deal with local time.
# because I want to compare 2 local times here (e.g. to know what the date boundary is)
# useful bits:
now = timezone.now() # noqa
default_timezone = timezone.get_current_timezone()
timezone.localtime(value, default_timezone)

View File

@@ -31,7 +31,7 @@
{% tailwind_formfield form.theme_preference %}
{% tailwind_formfield form.language %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
</form>
</div>

View File

@@ -24,7 +24,7 @@
{% tailwind_formfield form.username %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md">{% translate "Save" %}</button>
<a href="{% url "user_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</form>

View File

@@ -7,7 +7,7 @@
{% block content %}
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600/50 dark:bg-slate-900/50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
<div class="text-center m-4">
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete User</h3>
@@ -21,7 +21,7 @@
<form method="post" action="." id="deleteForm">
{% csrf_token %}
<input type="hidden" name="action" id="deleteAction" value="">
<button type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">Confirm</button>
<button type="submit" class="font-bold py-2 px-4 rounded-sm bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring-3">Confirm</button>
</form>
</div>
</div>
@@ -46,7 +46,7 @@
{% comment %}
Our current invite-system is tied to either a team or a project; no "global" invites (yet).
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">Invite Member</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring-3 rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">Invite Member</a>
</div>
{% endcomment %}
</div>
@@ -77,10 +77,10 @@
<div class="flex justify-end">
{% if not request.user == user %}
{% if user.is_active %}
<button name="action" value="deactivate:{{ user.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Deactivate</button>
<button name="action" value="deactivate:{{ user.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">Deactivate</button>
{% else %}
<button name="action" value="activate:{{ user.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Activate</button>
<button type="button" class="delete-button font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring ml-4" data-user-id="{{ user.id }}">Delete</button>
<button name="action" value="activate:{{ user.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring-3 rounded-md">Activate</button>
<button type="button" class="delete-button font-bold py-2 px-4 rounded-sm bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring-3 ml-4" data-user-id="{{ user.id }}">Delete</button>
{% endif %}
{% endif %}
</div>