mirror of
https://github.com/bugsink/bugsink.git
synced 2025-12-30 09:50:11 -06:00
Merge branch 'main' into django-5-2
This commit is contained in:
3
.bandit
3
.bandit
@@ -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
|
||||
|
||||
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -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)
|
||||
|
||||
@@ -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()'
|
||||
|
||||
@@ -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()'
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -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
52
bsmain/future_python.py
Normal 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
|
||||
@@ -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
102
bsmain/utils.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
97
ingest/management/commands/vacuum_ingest_dir.py
Normal file
97
ingest/management/commands/vacuum_ingest_dir.py
Normal 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."
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 " %}<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 " %}<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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 " %}<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 " %}<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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
609
theme/static/css/dist/styles.css
vendored
609
theme/static/css/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
1631
theme/static_src/package-lock.json
generated
1631
theme/static_src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"@tailwindcss/postcss": {},
|
||||
"postcss-simple-vars": {},
|
||||
"postcss-nested": {}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -75,7 +75,6 @@ module.exports = {
|
||||
*/
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user