diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cc65e5..9246cd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] db: [sqlite, mysql, postgres] include: - db: mysql diff --git a/bsmain/management/commands/send_bomb.py b/bsmain/management/commands/send_bomb.py index 8caff5d..dc7ec56 100644 --- a/bsmain/management/commands/send_bomb.py +++ b/bsmain/management/commands/send_bomb.py @@ -107,7 +107,8 @@ class Command(BaseCommand): return data_bytes def br_bomb(self, header, size): - filename = "/tmp/br-bomb-%d" % size + # no_bandit_expl utility-script for local consumtion only + filename = "/tmp/br-bomb-%d" % size # nosec B108 if os.path.exists(filename): with open(filename, "rb") as f: data_bytes = f.read() @@ -151,7 +152,7 @@ class Command(BaseCommand): def zlib_bomb(self, header, size, wbits): algo = "gzip" if wbits == WBITS_PARAM_FOR_GZIP else "deflate" - filename = "/tmp/%s-bomb-%d" % (algo, size) + filename = "/tmp/%s-bomb-%d" % (algo, size) # nosec B108 if os.path.exists(filename): with open(filename, "rb") as f: diff --git a/bugsink/conf_utils.py b/bugsink/conf_utils.py index bcdab98..3421a18 100644 --- a/bugsink/conf_utils.py +++ b/bugsink/conf_utils.py @@ -91,6 +91,7 @@ def eat_your_own_dogfood(sentry_dsn, **kwargs): "ee", "ingest", "issues", + "files", "performance", "phonehome", "projects", diff --git a/bugsink/settings/development.py b/bugsink/settings/development.py index 45b8928..48e86c4 100644 --- a/bugsink/settings/development.py +++ b/bugsink/settings/development.py @@ -149,6 +149,10 @@ LOGGING["handlers"]["snappea"]["level"] = "DEBUG" LOGGING["loggers"]["snappea"]["level"] = "DEBUG" LOGGING["formatters"]["snappea"]["format"] = "{asctime} - {threadName} - {levelname:7} - {message}" +# email logger: we mirror the advised logger from #86 here to debug that setting itself as well as get insight in email +# sending during development +LOGGING['loggers']['bugsink.email']['level'] = "INFO" + ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"]) # django-tailwind setting; the below allows for environment-variable overriding of the npm binary path. diff --git a/bugsink/utils.py b/bugsink/utils.py index 6df2428..80f05df 100644 --- a/bugsink/utils.py +++ b/bugsink/utils.py @@ -22,6 +22,12 @@ logger = logging.getLogger("bugsink.email") def send_rendered_email(subject, base_template_name, recipient_list, context=None): from phonehome.models import Installation + # Clean up; Django will do the same: https://github.com/django/django/blob/main/django/core/mail/message.py#L354 + # However, by doing the filter here we avoid logging something that is not reflective of what will actually happen. + recipient_list = [r for r in recipient_list if r] + if not recipient_list: + return + if not Installation.check_and_inc_email_quota(timezone.now()): logger.warning( "Email quota exceeded; not sending email with subject '%s' to %s", diff --git a/bugsink/wsgi.py b/bugsink/wsgi.py index 2130b3b..b1a04ed 100644 --- a/bugsink/wsgi.py +++ b/bugsink/wsgi.py @@ -1,12 +1,3 @@ -""" -WSGI config for bugsink project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ -""" - import os import django diff --git a/projects/views.py b/projects/views.py index 16d3d49..fb4111b 100644 --- a/projects/views.py +++ b/projects/views.py @@ -35,28 +35,30 @@ User = get_user_model() @atomic_for_request_method def project_list(request, ownership_filter=None): my_memberships = ProjectMembership.objects.filter(user=request.user) - my_team_memberships = TeamMembership.objects.filter(user=request.user) + # using `id__in` here to ensure the counts later on is not restricted to our own memberships (at most 1) my_projects = Project.objects.filter( - projectmembership__in=my_memberships, is_deleted=False).order_by('name').distinct() + id__in=ProjectMembership.objects.filter(user=request.user).values('project_id'), is_deleted=False) \ + .order_by('name').distinct() + my_teams_projects = \ Project.objects \ - .filter(team__teammembership__in=my_team_memberships, is_deleted=False) \ + .filter(team_id__in=TeamMembership.objects.filter(user=request.user).values('team_id'), is_deleted=False) \ .exclude(projectmembership__in=my_memberships) \ .order_by('name').distinct() if request.user.is_superuser: - # superusers can see all project, even hidden ones + # superusers can see all projects, even hidden ones other_projects = Project.objects \ .filter(is_deleted=False) \ - .exclude(projectmembership__in=my_memberships) \ - .exclude(team__teammembership__in=my_team_memberships) \ + .exclude(id__in=ProjectMembership.objects.filter(user=request.user).values('project_id')) \ + .exclude(team_id__in=TeamMembership.objects.filter(user=request.user).values('team_id')) \ .order_by('name').distinct() else: other_projects = Project.objects \ .filter(is_deleted=False) \ - .exclude(projectmembership__in=my_memberships) \ - .exclude(team__teammembership__in=my_team_memberships) \ + .exclude(id__in=ProjectMembership.objects.filter(user=request.user).values('project_id')) \ + .exclude(team_id__in=TeamMembership.objects.filter(user=request.user).values('team_id')) \ .exclude(visibility=ProjectVisibility.TEAM_MEMBERS) \ .order_by('name').distinct() diff --git a/pyproject.toml b/pyproject.toml index 82cecf0..bd3806c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dynamic = ["version", "dependencies"] diff --git a/teams/views.py b/teams/views.py index 6a39141..37e34e2 100644 --- a/teams/views.py +++ b/teams/views.py @@ -25,7 +25,9 @@ User = get_user_model() @atomic_for_request_method def team_list(request, ownership_filter=None): my_memberships = TeamMembership.objects.filter(user=request.user) - my_teams = Team.objects.filter(teammembership__in=my_memberships) + + # using `id__in` here to ensure the member_count later on is not restricted to our own memberships (at most 1) + my_teams = Team.objects.filter(id__in=TeamMembership.objects.filter(user=request.user).values('team_id')) if request.user.is_superuser: # superusers can see all teams, even hidden ones