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
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ RUN chmod +x startup.sh
EXPOSE 8000

# Run startup script
CMD ["./startup.sh", "gunicorn", "--bind", "0.0.0.0:8000", "--timeout", "180", "--workers", "2", "tally.wsgi:application"]
CMD ["./startup.sh", "gunicorn", "--bind", "0.0.0.0:8000", "--timeout", "180", "--workers", "2", "--access-logfile", "-", "--error-logfile", "-", "--capture-output", "--log-level", "info", "tally.wsgi:application"]
4 changes: 2 additions & 2 deletions backend/deploy-apprunner-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU
"TWITTER_REDIRECT_URI": "$SSM_PREFIX/$SSM_ENV/twitter_redirect_uri",
"DISCORD_REDIRECT_URI": "$SSM_PREFIX/$SSM_ENV/discord_redirect_uri"
},
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application"
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 --access-logfile - --error-logfile - --capture-output --log-level info tally.wsgi:application"
},
"ImageRepositoryType": "ECR"
},
Expand Down Expand Up @@ -271,7 +271,7 @@ EOF
"TWITTER_REDIRECT_URI": "$SSM_PREFIX/$SSM_ENV/twitter_redirect_uri",
"DISCORD_REDIRECT_URI": "$SSM_PREFIX/$SSM_ENV/discord_redirect_uri"
},
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application"
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 --access-logfile - --error-logfile - --capture-output --log-level info tally.wsgi:application"
},
"ImageRepositoryType": "ECR"
},
Expand Down
4 changes: 2 additions & 2 deletions backend/deploy-apprunner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU
"DISCORD_ROLE_SUBMISSION_SYNC_GRACE_SECONDS": "$SSM_PREFIX/prod/discord_role_submission_sync_grace_seconds",
"DISCORD_REDIRECT_URI": "$SSM_PREFIX/prod/discord_redirect_uri"
},
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application"
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 --access-logfile - --error-logfile - --capture-output --log-level info tally.wsgi:application"
},
"ImageRepositoryType": "ECR"
},
Expand Down Expand Up @@ -335,7 +335,7 @@ else
"DISCORD_ROLE_SUBMISSION_SYNC_GRACE_SECONDS": "$SSM_PREFIX/prod/discord_role_submission_sync_grace_seconds",
"DISCORD_REDIRECT_URI": "$SSM_PREFIX/prod/discord_redirect_uri"
},
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application"
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 --access-logfile - --error-logfile - --capture-output --log-level info tally.wsgi:application"
},
"ImageRepositoryType": "ECR"
},
Expand Down
68 changes: 66 additions & 2 deletions backend/projects/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html, format_html_join

from utils.admin_mixins import CloudinaryUploadMixin

Expand Down Expand Up @@ -31,7 +33,7 @@ class ProjectAdmin(CloudinaryUploadMixin, admin.ModelAdmin):
search_fields = ('title', 'slug', 'description', 'details', 'user__name', 'user__address')
list_editable = ('order', 'status')
raw_id_fields = ('user',)
filter_horizontal = ('participants', 'related_contributions')
autocomplete_fields = ('participants', 'related_contributions')
prepopulated_fields = {'slug': ('title',)}
readonly_fields = (
'created_at',
Expand All @@ -40,6 +42,8 @@ class ProjectAdmin(CloudinaryUploadMixin, admin.ModelAdmin):
'hero_image_tablet_public_id',
'hero_image_mobile_public_id',
'user_profile_image_public_id',
'selected_participants',
'selected_related_contributions',
)
ordering = ('order', '-created_at')

Expand All @@ -48,7 +52,13 @@ class ProjectAdmin(CloudinaryUploadMixin, admin.ModelAdmin):
'fields': ('title', 'slug', 'author', 'description', 'status', 'order'),
}),
('Relations', {
'fields': ('user', 'participants', 'related_contributions'),
'fields': (
'user',
'participants',
'selected_participants',
'related_contributions',
'selected_related_contributions',
),
}),
('Project Detail', {
'fields': (
Expand All @@ -75,3 +85,57 @@ class ProjectAdmin(CloudinaryUploadMixin, admin.ModelAdmin):
'classes': ('collapse',),
}),
)

@admin.display(description='Selected participants')
def selected_participants(self, obj):
if not obj or not obj.pk:
return 'Save the project before reviewing selected participants.'

participants = obj.participants.order_by('name', 'email', 'id')
if not participants.exists():
return 'No participants selected.'

rows = (
(
reverse('admin:users_user_change', args=[participant.pk]),
participant.name or participant.email or participant.address or f'User {participant.pk}',
participant.email or participant.address or '',
)
for participant in participants
)
return format_html(
'<ul style="margin: 0; padding-left: 18px;">{}</ul>',
format_html_join('', '<li><a href="{}">{}</a> <span style="color: #666;">{}</span></li>', rows),
)

@admin.display(description='Selected related contributions')
def selected_related_contributions(self, obj):
if not obj or not obj.pk:
return 'Save the project before reviewing selected related contributions.'

contributions = obj.related_contributions.select_related('user', 'contribution_type').order_by(
'user__name',
'user__email',
'-contribution_date',
'-created_at',
)
if not contributions.exists():
return 'No related contributions selected.'

rows = (
(
reverse('admin:contributions_contribution_change', args=[contribution.pk]),
contribution.title or f'Contribution {contribution.pk}',
contribution.user.name or contribution.user.email or contribution.user.address,
contribution.contribution_type.name,
)
for contribution in contributions
)
return format_html(
'<ul style="margin: 0; padding-left: 18px;">{}</ul>',
format_html_join(
'',
'<li><a href="{}">{}</a> <span style="color: #666;">by {} - {}</span></li>',
rows,
),
)
30 changes: 30 additions & 0 deletions backend/projects/tests/test_projects.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from datetime import timedelta

from django.contrib.admin.sites import AdminSite
from django.contrib.admin.widgets import AutocompleteSelectMultiple, FilteredSelectMultiple
from django.test import RequestFactory
from django.test import TestCase
from django.utils import timezone

from contributions.models import Category, Contribution, ContributionType, FeaturedContent
from projects.admin import ProjectAdmin
from projects.models import Project
from stewards.models import Steward
from users.models import User
Expand Down Expand Up @@ -278,3 +282,29 @@ def test_project_detail_404s_for_inactive_project(self):
response = self.client.get(f'/api/v1/projects/{project.slug}/')

self.assertEqual(response.status_code, 404)


class ProjectAdminTest(TestCase):
def test_project_relations_use_autocomplete_widgets(self):
admin_user = User.objects.create_superuser(
email='admin@example.com',
password='pass',
address='0x0000000000000000000000000000000000000009',
name='Admin User',
)
request = RequestFactory().get('/admin/projects/project/add/')
request.user = admin_user

project_admin = ProjectAdmin(Project, AdminSite())
form = project_admin.get_form(request)
readonly_fields = project_admin.get_readonly_fields(request)

participants_widget = form.base_fields['participants'].widget.widget
contributions_widget = form.base_fields['related_contributions'].widget.widget

self.assertIsInstance(participants_widget, AutocompleteSelectMultiple)
self.assertIsInstance(contributions_widget, AutocompleteSelectMultiple)
self.assertNotIsInstance(participants_widget, FilteredSelectMultiple)
self.assertNotIsInstance(contributions_widget, FilteredSelectMultiple)
self.assertIn('selected_participants', readonly_fields)
self.assertIn('selected_related_contributions', readonly_fields)
Loading