import json import re from functools import wraps from calendar import Calendar from datetime import date, datetime, time, timedelta from html import unescape from urllib.request import Request, urlopen from django.conf import settings from django.contrib import messages from django.contrib.auth import login from django.contrib.auth.decorators import login_required from django.db import transaction from django.db.models import Case, Count, IntegerField, ProtectedError, Q, Value, When from django.http import HttpResponse, JsonResponse from django.urls import reverse from django.utils import timezone from django.utils import translation from django.utils.translation import gettext as _ from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods, require_POST from .forms import ApiKeyForm, CollectionForm, EditItemForm, InviteRegistrationForm, KanbanForm, QuickItemForm, TeamForm, TeamInviteForm, UserInviteForm, UserPreferenceForm, UserSettingsForm from .models import ApiKey, Attachment, Collection, CollectionSection, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserInvite, UserPreference, TAG_RE, without_markdown_code TYPE_COMMAND_RE = re.compile(r'^/(todo|note|link|journal)\b', re.I) VISIBILITY_COMMAND_RE = re.compile(r'/(public|private)\b', re.I) URL_RE = re.compile(r'https?://\S+') ONLY_URL_RE = re.compile(r'^https?://\S+$') FIRST_LINE_URL_RE = re.compile(r'^(https?://\S+)[ \t]*(?:\r?\n|$)') TITLE_RE = re.compile(r']*>(.*?)', re.I | re.S) DATE_COMMAND_RE = re.compile(r'/datum\s+(\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4}))(?:\s+(\d{1,2}:\d{2}))?\b', re.I) RELATIVE_DATE_COMMAND_RE = re.compile(r'/(morgen|übermorgen|uebermorgen|nächstewoche|naechstewoche)\b', re.I) CODE_FENCE_RE = re.compile(r'^\s*(`{3,}|~{3,})') INLINE_CODE_RE = re.compile(r'(`+).*?\1', re.S) def mask_markdown_code(text): """Replace Markdown code with spaces while preserving string positions.""" masked_lines = [] fence = None for line in (text or '').splitlines(keepends=True): line_without_newline = line.rstrip('\r\n') newline = line[len(line_without_newline):] match = CODE_FENCE_RE.match(line_without_newline) if fence: masked_lines.append(' ' * len(line_without_newline) + newline) if match and match.group(1)[0] == fence[0] and len(match.group(1)) >= len(fence): fence = None elif match: fence = match.group(1) masked_lines.append(' ' * len(line_without_newline) + newline) else: masked_lines.append(INLINE_CODE_RE.sub(lambda m: ' ' * (m.end() - m.start()), line_without_newline) + newline) return ''.join(masked_lines) def remove_spans(text, spans): for start, end in sorted(spans, reverse=True): text = f'{text[:start]} {text[end:]}' return re.sub(r'[ \t]{2,}', ' ', text).strip() def fetch_page_title(url): try: request = Request(url, headers={'User-Agent': 'my2dos/1.0'}) with urlopen(request, timeout=4) as response: html = response.read(200_000).decode(response.headers.get_content_charset() or 'utf-8', errors='ignore') match = TITLE_RE.search(html) if match: return re.sub(r'\s+', ' ', unescape(match.group(1))).strip() except Exception: pass return '' def parse_quick_content(raw, default_kind=Item.Kind.NOTE): content = raw.strip() type_match = TYPE_COMMAND_RE.match(content) type_command = type_match.group(1).lower() if type_match else None masked_content = mask_markdown_code(content) visibility_matches = list(VISIBILITY_COMMAND_RE.finditer(masked_content)) visibility_commands = {match.group(1).lower() for match in visibility_matches} offset = 0 if type_match: remainder = content[type_match.end():] leading_space = len(remainder) - len(remainder.lstrip()) offset = type_match.end() + leading_space content = remainder.strip() visibility_spans = [(match.start() - offset, match.end() - offset) for match in visibility_matches if match.start() >= offset] content = remove_spans(content, visibility_spans) only_url = bool(ONLY_URL_RE.match(content)) first_line_url_match = FIRST_LINE_URL_RE.match(content) if type_command == 'todo': kind = Item.Kind.TODO elif type_command == 'journal': kind = Item.Kind.JOURNAL elif type_command == 'link' or only_url or first_line_url_match: kind = Item.Kind.LINK elif type_command == 'note': kind = Item.Kind.NOTE else: kind = default_kind if default_kind in Item.Kind.values else Item.Kind.NOTE visibility = Item.Visibility.PUBLIC if 'public' in visibility_commands else Item.Visibility.PRIVATE url_match = first_line_url_match or URL_RE.search(content) url = url_match.group(1) if kind == Item.Kind.LINK and first_line_url_match else (url_match.group(0) if kind == Item.Kind.LINK and url_match else '') if first_line_url_match: title = fetch_page_title(url) or url extra_content = content[first_line_url_match.end():].strip() content = f'{title}\n{extra_content}' if extra_content else title elif only_url: content = fetch_page_title(url) or url return kind, visibility, content, url def parse_due_command(raw): """Extract a due date slash command and return cleaned text, date and match state.""" masked_raw = mask_markdown_code(raw) date_match = DATE_COMMAND_RE.search(masked_raw) relative_match = RELATIVE_DATE_COMMAND_RE.search(masked_raw) match = date_match or relative_match if not match: return raw, None, False due_time = time(23, 59) if date_match: date_format = '%d.%m.%Y' if len(date_match.group(1).rsplit('.', 1)[-1]) == 4 else '%d.%m.%y' try: due_date = datetime.strptime(date_match.group(1), date_format).date() if date_match.group(2): due_time = time.fromisoformat(date_match.group(2)) except ValueError: return raw, None, False else: today = timezone.localdate() command = relative_match.group(1).lower() if command == 'morgen': due_date = today + timedelta(days=1) elif command in {'übermorgen', 'uebermorgen'}: due_date = today + timedelta(days=2) else: due_date = today + timedelta(days=(7 - today.weekday())) cleaned = remove_spans(raw, [(match.start(), match.end())]) return cleaned, timezone.make_aware(datetime.combine(due_date, due_time)), True def parse_due_at_from_post(post): due_date = (post.get('due_date') or '').strip() due_time = (post.get('due_time') or '').strip() if not due_date: return None try: parsed_date = date.fromisoformat(due_date) parsed_time = time.fromisoformat(due_time) if due_time else time(23, 59) return timezone.make_aware(datetime.combine(parsed_date, parsed_time)) except ValueError: return None def delete_selected_attachments(item, attachment_ids): for attachment in item.attachments.filter(pk__in=attachment_ids): attachment.file.delete(save=False) attachment.delete() if hasattr(item, '_prefetched_objects_cache'): item._prefetched_objects_cache.pop('attachments', None) def attach_files_and_replace_tokens(item, files): replacements = {} attached = False for file in files: attachment = Attachment.objects.create(item=item, file=file) attached = True replacements[f'![[paste:{file.name}]]'] = f'![[file:{attachment.id}]]' if attached and hasattr(item, '_prefetched_objects_cache'): item._prefetched_objects_cache.pop('attachments', None) if replacements: content = item.content comment = item.comment for old, new in replacements.items(): content = content.replace(old, new) comment = comment.replace(old, new) if content != item.content or comment != item.comment: item.content = content item.comment = comment item.save(update_fields=['content', 'comment', 'updated_at']) def user_team_ids(user): if not user.is_authenticated: return [] return list(user.team_memberships.values_list('team_id', flat=True)) def visible_items(user, api_key=None): qs = Item.objects.select_related('owner', 'team').prefetch_related('tags', 'attachments', 'linked_items') if api_key: qs = qs.filter(owner=api_key.owner) allowed_tags = api_key.tags.all() if allowed_tags.exists(): qs = qs.filter(tags__in=allowed_tags).distinct() return qs if user.is_authenticated: return qs.filter(Q(owner=user) | Q(team_id__in=user_team_ids(user)) | Q(visibility=Item.Visibility.PUBLIC)).distinct() return qs.filter(visibility=Item.Visibility.PUBLIC) def user_can_edit_item(user, item): return item.owner_id == user.id or (item.team_id and TeamMembership.objects.filter(team=item.team, user=user).exists()) def user_can_delete_item(user, item): return item.owner_id == user.id or (item.team_id and TeamMembership.objects.filter(team=item.team, user=user, role__in=[TeamMembership.Role.OWNER, TeamMembership.Role.ADMIN]).exists()) def desired_item_scope(item, user, force_private=False): if force_private: return Item.Visibility.PRIVATE, None if item.visibility == Item.Visibility.PUBLIC: return Item.Visibility.PUBLIC, None tag_names = {match.group(1).lower() for match in re.finditer(r'(? 1: query = request.GET.copy() query['tag'] = q[1:].strip().lower() query.pop('q', None) return redirect(f'{request.path}?{query.urlencode()}') items = visible_items(request.user) applied_preferences = False if active_tags: if active_tag_mode == 'all': for tag_name in active_tags: items = items.filter(tags__name=tag_name) else: items = items.filter(tags__name__in=active_tags).distinct() if kind in dict(Item.Kind.choices): items = items.filter(kind=kind) elif not any([tag, status, q, day]): applied_preferences = True type_filter = Q() if prefs.show_notes: type_filter |= Q(kind=Item.Kind.NOTE) if prefs.show_links: type_filter |= Q(kind=Item.Kind.LINK) if prefs.show_journal: type_filter |= Q(kind=Item.Kind.JOURNAL) if prefs.show_open_todos: type_filter |= Q(kind=Item.Kind.TODO, is_done=False) if prefs.show_done_todos: type_filter |= Q(kind=Item.Kind.TODO, is_done=True) items = items.filter(type_filter) if type_filter else items.none() if day: items = items.filter(created_at__date=day) if status == 'open': items = items.filter(kind=Item.Kind.TODO, is_done=False) elif status == 'due': today_end = timezone.make_aware(datetime.combine(timezone.localdate(), time(23, 59, 59))) items = items.filter(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end).order_by('due_at', '-created_at') elif status == 'archive': items = items.filter(kind=Item.Kind.TODO, is_done=True) elif not applied_preferences: items = items.exclude(kind=Item.Kind.TODO, is_done=True) if q: items = items.filter(Q(content__icontains=q) | Q(url__icontains=q) | Q(tags__name__icontains=q)).distinct() today_end = timezone.make_aware(datetime.combine(timezone.localdate(), time(23, 59, 59))) due_count = items.filter(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end).count() if prefs.due_first and status != 'archive': items = items.annotate(due_rank=Case( When(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end, then=Value(0)), default=Value(1), output_field=IntegerField(), )).order_by('due_rank', 'due_at', '-created_at') return render(request, 'core/index.html', { 'form': QuickItemForm(), 'items': items, 'tags': index_filter_tags(request, request.user, prefs, active_tags), 'active_tag': tag, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode, 'active_kind': kind, 'active_status': status, 'active_day': day, 'q': q, 'due_count': due_count, 'overview_lines': prefs.overview_lines, }) @login_required def new_item(request): prefs, created = UserPreference.objects.get_or_create(user=request.user) requested_kind = request.GET.get('kind') or prefs.default_item_kind next_url = request.GET.get('next') or request.POST.get('next') or reverse('index') if next_url == 'index': next_url = reverse('index') collection = None collection_id = request.GET.get('collection') or request.POST.get('collection') if collection_id: collection = get_object_or_404(visible_collections(request.user).filter(mode__in=Collection.STRUCTURED_MODES), pk=collection_id) if not user_can_edit_collection(request.user, collection): return JsonResponse({'error': 'forbidden'}, status=403) requested_kind = Item.Kind.NOTE if request.method == 'POST': form = QuickItemForm(request.POST, request.FILES) if form.is_valid(): raw = form.cleaned_data['content'] if requested_kind in {'todo', 'link', 'journal'} and not TYPE_COMMAND_RE.match(raw.strip()): raw = f'/{requested_kind} {raw}' raw, due_at, _ = parse_due_command(raw) kind, visibility, content, url = parse_quick_content(raw, prefs.default_item_kind) item = Item.objects.create(owner=request.user, kind=kind, visibility=visibility, content=content, url=url, due_at=due_at) attach_files_and_replace_tokens(item, request.FILES.getlist('files')) item.sync_metadata() apply_team_from_tags(item, request.user, force_private=bool(re.search(r'/private\b', raw, re.I))) if collection: item = adapt_item_for_collection(item, collection) add_collection_section(collection, item) return redirect(next_url) return render(request, 'core/new_item.html', { 'form': QuickItemForm(), 'tags': Tag.objects.all(), 'requested_kind': requested_kind, 'next_url': next_url, 'collection': collection, }) @login_required def journal(request): if request.method == 'POST': form = QuickItemForm(request.POST, request.FILES) if form.is_valid(): raw = form.cleaned_data['content'] if not TYPE_COMMAND_RE.match(raw.strip()): raw = f'/journal {raw}' raw, due_at, _ = parse_due_command(raw) kind, visibility, content, url = parse_quick_content(raw) item = Item.objects.create(owner=request.user, kind=kind, visibility=visibility, content=content, url=url, due_at=due_at) attach_files_and_replace_tokens(item, request.FILES.getlist('files')) item.sync_metadata() apply_team_from_tags(item, request.user, force_private=bool(re.search(r'/private\b', raw, re.I))) if request.headers.get('HX-Request'): return render(request, 'core/_item.html', {'item': item, 'overview_lines': request.user.preferences.overview_lines if hasattr(request.user, 'preferences') else 4}) return redirect('journal') today = date.today() selected_day = request.GET.get('day') or today.isoformat() selected = date.fromisoformat(selected_day) year = int(request.GET.get('year', selected.year)) month = int(request.GET.get('month', selected.month)) base = visible_items(request.user) journal_entries = base.filter(kind=Item.Kind.JOURNAL, created_at__date=selected) created_entries = base.filter(kind__in=[Item.Kind.NOTE, Item.Kind.LINK], created_at__date=selected) completed_todos = base.filter(kind=Item.Kind.TODO, completed_at__date=selected) items = (journal_entries | created_entries | completed_todos).distinct().order_by('-created_at') activity_dates = set(base.filter(kind__in=[Item.Kind.JOURNAL, Item.Kind.NOTE, Item.Kind.LINK], created_at__year=year, created_at__month=month).dates('created_at', 'day')) activity_dates |= set(base.filter(kind=Item.Kind.TODO, completed_at__year=year, completed_at__month=month).dates('completed_at', 'day')) weeks = [[{'day': d, 'date': date(year, month, d).isoformat() if d else '', 'has_entries': d and date(year, month, d) in activity_dates} for d in week] for week in Calendar(firstweekday=0).monthdayscalendar(year, month)] prev_month, prev_year = (12, year - 1) if month == 1 else (month - 1, year) next_month, next_year = (1, year + 1) if month == 12 else (month + 1, year) prefs, created = UserPreference.objects.get_or_create(user=request.user) return render(request, 'core/journal.html', { 'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'weeks': weeks, 'year': year, 'month': month, 'selected_day': selected_day, 'prev_year': prev_year, 'prev_month': prev_month, 'next_year': next_year, 'next_month': next_month, 'overview_lines': prefs.overview_lines, }) @require_POST def set_user_language(request): language = request.POST.get('language') next_url = request.POST.get('next') or '/' if language not in {'de', 'en', ''}: language = '' if request.user.is_authenticated: prefs, created = UserPreference.objects.get_or_create(user=request.user) prefs.language = language prefs.save(update_fields=['language']) response = redirect(next_url) if language: translation.activate(language) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language) else: response.delete_cookie(settings.LANGUAGE_COOKIE_NAME) return response @login_required def settings_view(request): if request.method == 'POST': if request.POST.get('action') == 'profile': form = UserSettingsForm(request.POST, instance=request.user) if form.is_valid(): form.save() messages.success(request, _('Settings saved.')) return redirect('settings') elif request.POST.get('action') == 'preferences': prefs, created = UserPreference.objects.get_or_create(user=request.user) pref_form = UserPreferenceForm(request.POST, instance=prefs) if pref_form.is_valid(): pref_form.save() messages.success(request, _('Default view saved.')) return redirect('settings') elif request.POST.get('action') == 'apikey': key_form = ApiKeyForm(request.POST) if key_form.is_valid(): api_key = key_form.save(commit=False) api_key.owner = request.user api_key.save() key_form.save_m2m() messages.success(request, _('API key created: %(token)s') % {'token': api_key.token}) return redirect('settings') elif request.POST.get('action') == 'userinvite': invite_form = UserInviteForm(request.POST) if invite_form.is_valid(): invite = invite_form.save(commit=False) invite.invited_by = request.user invite.save() messages.success(request, _('Invitation link created: %(url)s') % {'url': request.build_absolute_uri(reverse("accept_user_invite", args=[invite.token]))}) return redirect('settings') prefs, created = UserPreference.objects.get_or_create(user=request.user) return render(request, 'core/settings.html', { 'profile_form': UserSettingsForm(instance=request.user), 'preference_form': UserPreferenceForm(instance=prefs), 'key_form': ApiKeyForm(), 'user_invite_form': UserInviteForm(), 'user_invites': request.user.sent_user_invites.filter(accepted_at__isnull=True), 'api_keys': request.user.api_keys.prefetch_related('tags'), }) @login_required @require_POST def delete_api_key(request, pk): api_key = get_object_or_404(ApiKey, pk=pk, owner=request.user) api_key.delete() return redirect('settings') @login_required @require_POST def delete_user_invite(request, pk): invite = get_object_or_404(UserInvite, pk=pk, invited_by=request.user, accepted_at__isnull=True) invite.delete() if request.headers.get('HX-Request'): return HttpResponse('') messages.success(request, _('Invitation deleted.')) return redirect('settings') def accept_user_invite(request, token): invite = get_object_or_404(UserInvite, token=token, accepted_at__isnull=True) if request.user.is_authenticated: invite.accepted_at = timezone.now() invite.save(update_fields=['accepted_at']) messages.success(request, _('Invitation accepted.')) return redirect('index') if request.method == 'POST': form = InviteRegistrationForm(request.POST) if form.is_valid(): user = form.save(commit=False) if invite.email and not user.email: user.email = invite.email user.save() invite.accepted_at = timezone.now() invite.save(update_fields=['accepted_at']) login(request, user) messages.success(request, _('Welcome to my2dos.')) return redirect('index') else: form = InviteRegistrationForm(initial={'email': invite.email}) return render(request, 'core/accept_user_invite.html', {'invite': invite, 'form': form}) @login_required def team_list(request): if request.method == 'POST': team_form = TeamForm(request.POST) if team_form.is_valid(): team = team_form.save(commit=False) team.owner = request.user team.save() TeamMembership.objects.create(team=team, user=request.user, role=TeamMembership.Role.OWNER) Tag.objects.get_or_create(name=team.slug) messages.success(request, _('Team %(team)s created. Use #%(slug)s for team items.') % {'team': team.name, 'slug': team.slug}) return redirect('team_detail', pk=team.pk) teams = Team.objects.filter(memberships__user=request.user).distinct().prefetch_related('memberships__user') stats = [] base_items = visible_items(request.user) for team in teams: team_items = base_items.filter(team=team) stats.append({ 'team': team, 'members_count': team.memberships.count(), 'items_count': team_items.count(), 'open_todos_count': team_items.filter(kind=Item.Kind.TODO, is_done=False).count(), 'recent_items': team_items[:3], }) return render(request, 'core/team_list.html', {'stats': stats, 'team_form': TeamForm(), 'overview_lines': request.user.preferences.overview_lines if hasattr(request.user, 'preferences') else 4}) @login_required def team_detail(request, pk): membership = get_object_or_404(TeamMembership.objects.select_related('team'), team_id=pk, user=request.user) team = membership.team if request.method == 'POST' and membership.role in [TeamMembership.Role.OWNER, TeamMembership.Role.ADMIN]: invite_form = TeamInviteForm(request.POST) if invite_form.is_valid(): invite = invite_form.save(commit=False) invite.team = team invite.invited_by = request.user invite.save() messages.success(request, _('Invitation link created: %(url)s') % {'url': request.build_absolute_uri(reverse("team_accept_invite", args=[invite.token]))}) return redirect('team_detail', pk=team.pk) items = visible_items(request.user).filter(team=team) return render(request, 'core/team_detail.html', { 'team': team, 'membership': membership, 'memberships': team.memberships.select_related('user'), 'invites': team.invites.filter(accepted_at__isnull=True), 'invite_form': TeamInviteForm(), 'items': items, 'overview_lines': request.user.preferences.overview_lines if hasattr(request.user, 'preferences') else 4, }) @login_required def team_accept_invite(request, token): invite = get_object_or_404(TeamInvite.objects.select_related('team'), token=token, accepted_at__isnull=True) TeamMembership.objects.get_or_create(team=invite.team, user=request.user, defaults={'role': TeamMembership.Role.MEMBER}) invite.accepted_at = timezone.now() invite.save(update_fields=['accepted_at']) messages.success(request, _('You are now a member of team %(team)s.') % {'team': invite.team.name}) return redirect('team_detail', pk=invite.team.pk) def visible_collections(user): qs = Collection.objects.select_related('owner', 'team').prefetch_related('filter_tags', 'sections__item', 'sections__sub_collection') if user.is_authenticated: return qs.filter(Q(owner=user) | Q(team_id__in=user_team_ids(user)) | Q(visibility=Item.Visibility.PUBLIC)).distinct() return qs.filter(visibility=Item.Visibility.PUBLIC) def user_can_edit_collection(user, collection): return user.is_authenticated and (collection.owner_id == user.id or (collection.team_id and TeamMembership.objects.filter(team=collection.team, user=user).exists())) def user_can_delete_collection(user, collection): return user.is_authenticated and (collection.owner_id == user.id or (collection.team_id and TeamMembership.objects.filter(team=collection.team, user=user, role__in=[TeamMembership.Role.OWNER, TeamMembership.Role.ADMIN]).exists())) def collection_scope_allows_item(collection_visibility, collection_team_id, collection_owner_id, item_visibility, item_team_id, item_owner_id): # A collection may reference a note that is already visible to a wider audience. if item_visibility == Item.Visibility.PUBLIC: return True if collection_visibility == Item.Visibility.PUBLIC: return False if collection_visibility == Item.Visibility.TEAM: return item_visibility == Item.Visibility.TEAM and item_team_id == collection_team_id return item_visibility == Item.Visibility.PRIVATE and item_owner_id == collection_owner_id def collection_item_is_compatible(collection, item): return collection_scope_allows_item( collection.visibility, collection.team_id, collection.owner_id, item.visibility, item.team_id, item.owner_id, ) def collection_subcollection_is_compatible(collection, sub_collection): return collection_scope_allows_item( collection.visibility, collection.team_id, collection.owner_id, sub_collection.visibility, sub_collection.team_id, sub_collection.owner_id, ) def collection_contains_collection(collection, target): if collection.pk == target.pk: return True for section in collection.sections.select_related('sub_collection'): if section.sub_collection and collection_contains_collection(section.sub_collection, target): return True return False def collection_target_from_form(form, user): team = form.cleaned_data.get('team') if team: return Item.Visibility.TEAM, team return form.cleaned_data['visibility'], None def collection_tag_items(collection): items = Item.objects.filter(kind__in=collection.included_item_kinds()).select_related('owner', 'team').prefetch_related('tags', 'attachments') if collection.visibility == Item.Visibility.PUBLIC: items = items.filter(visibility=Item.Visibility.PUBLIC) elif collection.visibility == Item.Visibility.TEAM: items = items.filter(Q(visibility=Item.Visibility.PUBLIC) | Q(visibility=Item.Visibility.TEAM, team=collection.team)) else: items = items.filter(Q(visibility=Item.Visibility.PUBLIC) | Q(visibility=Item.Visibility.PRIVATE, owner=collection.owner)) for tag in collection.filter_tags.all(): items = items.filter(tags=tag) return items.distinct().order_by('created_at', 'id') def item_can_change_for_collection(user, item, collection): if not user_can_edit_item(user, item): return False target_team_id = collection.team_id for section in item.collection_sections.select_related('collection').exclude(collection=collection): other = section.collection if not collection_scope_allows_item( other.visibility, other.team_id, other.owner_id, collection.visibility, target_team_id, item.owner_id, ): return False return True def remove_team_tags(content): slugs = Team.objects.values_list('slug', flat=True) for slug in slugs: content = re.sub(rf'(?