Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Common task utilities are provided in `impresso/utils/tasks/__init__.py`:
- `is_task_stopped()` - Check if user has stopped a job

Task states:

- `TASKSTATE_INIT` - Task initialization
- `TASKSTATE_PROGRESS` - Task in progress
- `TASKSTATE_SUCCESS` - Task completed successfully
Expand Down Expand Up @@ -89,6 +90,7 @@ Task states:
- Use structured logging with task context (job_id, user_id)

Example task pattern:

```python
from celery import shared_task
from celery.utils.log import get_task_logger
Expand Down Expand Up @@ -131,8 +133,6 @@ def my_task(self, user_id: int) -> None:
- Handle SMTP exceptions gracefully
- Log email sending status



### Job Management

- Jobs track long-running asynchronous tasks
Expand Down Expand Up @@ -203,7 +203,10 @@ ENV=dev pipenv run celery -A impresso worker -l info
pipenv run mypy --config-file ./.mypy.ini impresso
```

### Common Commands
### Django custom Commands

- Custom management commands are located in `impresso/management/commands/`
- Example commands include creating accounts, stopping jobs, and updating user bitmaps.

```bash
# Create accounts
Expand All @@ -216,6 +219,23 @@ ENV=dev pipenv run ./manage.py stopjob <job_id>
ENV=dev pipenv run ./manage.py updateuserbitmap <user_id>
```

Example command structure, use help and logging extensively for clarity nd ALWAYS use typings as much as possible:

```python
from django.core.management.base import BaseCommand

class Command(BaseCommand):
help = "Check pending special membership requests for a user"

def add_arguments(self, parser) -> None:
parser.add_argument("username", type=str)

def handle(self, username:str, *args, **options) -> None:
self.stdout.write(f"Get user with username: {username}")
```

- when creating a new command, be

## Security Considerations

- Never commit secrets to source code
Expand Down Expand Up @@ -250,3 +270,13 @@ When adding new Celery tasks:
- Main repository: https://github.com/impresso/impresso-user-admin
- Impresso project: https://impresso-project.ch
- License: GNU Affero General Public License v3.0

## Adding new email templates and sending emails

When adding new email templates:

1. Create both `.txt` and `.html` versions in `impresso/templates/emails/`
2. Use consistent naming conventions (e.g., `plan_change_request_email.txt` and `plan_change_request_email.html`)
3. Use context variables for dynamic content
4. use the method `send_templated_email_with_context()` from `impresso/utils/tasks/email.py` to send emails
5. Handle SMTP exceptions and log email sending status in STRERR appropriately using logging library
191 changes: 191 additions & 0 deletions impresso/management/commands/checkpendingrequests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from django.db.models import Q, QuerySet
from django.conf import settings
from impresso.models import UserSpecialMembershipRequest
from impresso.utils.tasks.email import send_templated_email_with_context
from typing import Optional


class Command(BaseCommand):
"""
Check pending special membership requests for reviewers.

The reviewer can be:
- Directly assigned to a UserSpecialMembershipRequest instance
- Assigned to a SpecialMembershipDataset instance (linked via subscription)

Usage with pipenv:
ENV=dev pipenv run ./manage.py checkpendingrequests
ENV=dev pipenv run ./manage.py checkpendingrequests <username>
ENV=dev pipenv run ./manage.py checkpendingrequests <username> --send-email
ENV=dev pipenv run ./manage.py checkpendingrequests <username> --send-email --dry-run

Usage with docker:
docker-compose exec <your image name> python manage.py checkpendingrequests
docker-compose exec <your image name> python manage.py checkpendingrequests <username>

Example:
ENV=dev pipenv run ./manage.py checkpendingrequests
ENV=dev pipenv run ./manage.py checkpendingrequests john.doe
ENV=dev pipenv run ./manage.py checkpendingrequests john.doe --send-email
ENV=dev pipenv run ./manage.py checkpendingrequests john.doe --send-email --dry-run

testing:
ENV=test pipenv run ./manage.py test impresso.tests.management.test_checkpendingrequests
"""

help = "Check pending special membership requests for a specific reviewer"

def add_arguments(self, parser) -> None:
parser.add_argument(
"username",
nargs="?",
type=str,
help="Optional username of specific reviewer to check pending requests for",
)

parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be sent without actually sending emails",
)

def handle(self, username: Optional[str] = None, *args, **options) -> None:
dry_run: bool = options.get("dry_run", False)

if username:
self.stdout.write(
f"Looking up reviewer with username: \033[1m{username}\033[0m\n"
)
else:
self.stdout.write("Checking all reviewers for pending requests\n")
if dry_run:
self.stdout.write(
self.style.WARNING("DRY RUN MODE - No emails will be sent")
)

# Fetch the reviewer user(s)
if username:
try:
reviewers = [User.objects.get(username=username)]
except User.DoesNotExist:
self.stdout.write(
self.style.ERROR(
f"✗ User with username '{username}' does not exist.\n"
)
)
return
else:
# Get all users who have pending requests
all_pending_requests: QuerySet[UserSpecialMembershipRequest] = (
UserSpecialMembershipRequest.objects.filter(
status=UserSpecialMembershipRequest.STATUS_PENDING
)
)
# get all reviewers or subscription__reviewer
reviewers_ids = set(
all_pending_requests.values_list("reviewer_id", flat=True)
).union(
set(
all_pending_requests.values_list(
"subscription__reviewer_id", flat=True
)
)
)
reviewers = User.objects.filter(id__in=reviewers_ids)

if not reviewers:
self.stdout.write(
self.style.WARNING("No reviewers with pending requests found.\n")
)
return

# Process each reviewer
for reviewer in reviewers:
self._process_reviewer(reviewer, dry_run)

def _process_reviewer(self, reviewer: User, dry_run: bool = False) -> None:
"""Process a single reviewer and display their pending requests."""
self.stdout.write(f"\nReviewer: \033[1m{reviewer.username}\033[0m")

# Query pending requests for this reviewer
pending_requests: QuerySet[UserSpecialMembershipRequest] = (
UserSpecialMembershipRequest.objects.filter(
status=UserSpecialMembershipRequest.STATUS_PENDING
)
.filter(Q(reviewer=reviewer) | Q(subscription__reviewer=reviewer))
.select_related("user", "reviewer", "subscription")
.order_by("-date_created")
)

request_count: int = pending_requests.count()

if request_count == 0:
self.stdout.write(" No pending requests")
return

self.stdout.write(
self.style.WARNING(f"Found {request_count} pending request(s)")
)

for idx, request in enumerate(pending_requests):
self.stdout.write(f"\n{idx + 1}. Request ID: \033[1m{request.pk}\033[0m")
self.stdout.write(
f" User: {request.user.username} ({request.user.get_full_name() or 'No name'})"
)
self.stdout.write(f" Email: {request.user.email}")
self.stdout.write(
f" Subscription: {request.subscription.title if request.subscription else '[deleted subscription]'}"
)
# Determine reviewer assignment type
if request.reviewer == reviewer:
self.stdout.write(
f" Reviewer assignment: \033[1mDirect\033[0m (assigned to request)"
)
elif request.subscription and request.subscription.reviewer == reviewer:
self.stdout.write(
f" Reviewer assignment: \033[1mDataset-level\033[0m (assigned to subscription dataset)"
)
else:
self.stdout.write(f" Reviewer assignment: \033[1mOther\033[0m")

self.stdout.write(f" Status: {request.status}")

# Send email if requested
if dry_run:
self.stdout.write(
self.style.WARNING(f"\n[DRY RUN] Would send email to {reviewer.email}")
)
return
if not reviewer.email:
self.stdout.write(self.style.WARNING(f" ✗ No email address configured"))
return
# Get the 3 most recent requests
latest_requests = list(pending_requests[:3])
# Prepare email context
context = {
"reviewer": reviewer,
"latest_requests": latest_requests,
"total_count": request_count,
"settings": settings,
}

# Send email
try:
success = send_templated_email_with_context(
template="pending_requests_summary_to_reviewer",
subject=f"Impresso: {request_count} Pending Special Membership Request{'s' if request_count != 1 else ''}",
from_email=settings.DEFAULT_FROM_EMAIL,
to=[reviewer.email],
context=context,
)

if success:
self.stdout.write(
self.style.SUCCESS(f"\n✓ Email sent to {reviewer.email}")
)
else:
self.stdout.write(self.style.ERROR(f" ✗ Failed to send email"))
except Exception as e:
self.stdout.write(self.style.ERROR(f" ✗ Error: {str(e)}"))
Loading