Skip to content

Commit dddc128

Browse files
Merge branch 'organization-collective' into staging
2 parents 0566c86 + 2983010 commit dddc128

File tree

25 files changed

+963
-360
lines changed

25 files changed

+963
-360
lines changed

.github/workflows/lambda.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Post-Deploy Lambda
2+
3+
on:
4+
deployment:
5+
6+
jobs:
7+
deploy-lambdas:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v3
11+
12+
- name: Show deployment info
13+
run: |
14+
echo "Deployment environment: $DEPLOYMENT_ENVIRONMENT"
15+
16+
- name: Run Lambda deploy
17+
run: |
18+
if [[ "$DEPLOYMENT_ENVIRONMENT" == "documentcloud-staging" ]]; then
19+
echo "Deploying staging lambda updates"
20+
bash config/aws/lambda/codeship_deploy_lambdas.sh staging-lambda --staging
21+
else
22+
echo "Deploying production lambda updates"
23+
bash config/aws/lambda/codeship_deploy_lambdas.sh prod-lambda
24+
fi

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,7 @@ embedding/
300300
# solr
301301
!config/solr/lib/*
302302

303-
scratch/
303+
scratch/
304+
305+
CLAUDE.md
306+
.claude

Procfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
release: ./postdeploy.sh
12
web: bin/start-nginx gunicorn -c config/gunicorn.conf config.wsgi:application
23
worker: REMAP_SIGTERM=SIGQUIT celery --app=config.celery_app worker --loglevel=info
34
solr_worker: REMAP_SIGTERM=SIGQUIT celery --app=config.celery_app worker --loglevel=info -Q solr,celery
4-
beat: REMAP_SIGTERM=SIGQUIT celery --app=config.celery_app beat --loglevel=info
5+
beat: REMAP_SIGTERM=SIGQUIT celery --app=config.celery_app beat --loglevel=info

config/settings/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@
290290
ADMINS = [("Mitchell Kotler", "mitch@muckrock.com")]
291291
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
292292
MANAGERS = ADMINS
293+
# Chunk size for CSV exports using .iterator() to process large querysets
294+
# without loading all records into memory at once
295+
CSV_EXPORT_CHUNK_SIZE = env.int("CSV_EXPORT_CHUNK_SIZE", default=2000)
293296

294297
# LOGGING
295298
# ------------------------------------------------------------------------------
@@ -389,8 +392,14 @@
389392
"task": "documentcloud.addons.tasks.dispatch_events",
390393
"schedule": crontab(minute="*/5"),
391394
},
395+
"permission_digest": {
396+
"task": "documentcloud.users.tasks.permission_digest",
397+
"schedule": crontab(day_of_week="mon", hour=7, minute=0),
398+
},
392399
}
393400

401+
PERMISSIONS_DIGEST_EMAILS = env.list("PERMISSIONS_DIGEST_EMAILS", default=[])
402+
394403
# django-compressor
395404
# ------------------------------------------------------------------------------
396405
# https://django-compressor.readthedocs.io/en/latest/quickstart/#installation
@@ -614,6 +623,7 @@
614623
CLOUDFLARE_API_EMAIL = env("CLOUDFLARE_API_EMAIL", default="")
615624
CLOUDFLARE_API_KEY = env("CLOUDFLARE_API_KEY", default="")
616625
CLOUDFLARE_API_ZONE = env("CLOUDFLARE_API_ZONE", default="")
626+
CLOUDFLARE_HOSTS = env.list("CLOUDFLARE_HOSTS", default=[])
617627

618628
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
619629

config/settings/production.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
# Third Party
66
import sentry_sdk
7-
from ipware import get_client_ip
87
from sentry_sdk.integrations.celery import CeleryIntegration
98
from sentry_sdk.integrations.django import DjangoIntegration
109
from sentry_sdk.integrations.logging import LoggingIntegration
@@ -74,6 +73,11 @@
7473
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
7574
AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None)
7675

76+
CLOUDFLARE_HOSTS = env.list(
77+
"CLOUDFLARE_HOSTS",
78+
default=["https://www.documentcloud.org", "https://embed.documentcloud.org"],
79+
)
80+
7781
# STATIC
7882
# ------------------------
7983
STATICFILES_STORAGE = "config.settings.production.StaticRootS3Boto3Storage"

documentcloud/addons/admin.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Django
2+
from django.conf import settings
23
from django.contrib import admin, messages
3-
from django.db.models import JSONField
4+
from django.db.models import JSONField, Q
45
from django.forms import widgets
56
from django.http import HttpResponse
67
from django.http.response import HttpResponseRedirect
@@ -124,11 +125,27 @@ class AddOnRunAdmin(admin.ModelAdmin):
124125
actions = ["export_runs_as_csv"]
125126
readonly_fields = [f.name for f in AddOnRun._meta.fields]
126127

128+
def get_search_results(self, request, queryset, search_term):
129+
"""Avoid collation issue when searching add-on runs"""
130+
if search_term:
131+
queryset = queryset.filter(
132+
Q(addon__name__icontains=search_term)
133+
| Q(user__email__icontains=search_term)
134+
| Q(status__icontains=search_term)
135+
)
136+
use_distinct = False
137+
else:
138+
use_distinct = False
139+
return queryset, use_distinct
140+
127141
def export_runs_as_csv(self, request, queryset):
128142
"""Export selected Add-On Runs to CSV."""
129143
field_names = [
130144
"addon_id",
145+
"addon_name",
131146
"user_id",
147+
"user_name",
148+
"user_email",
132149
"run_id",
133150
"status",
134151
"rating",
@@ -143,11 +160,28 @@ def export_runs_as_csv(self, request, queryset):
143160
writer = csv.writer(response)
144161
writer.writerow(field_names)
145162

146-
for run in queryset:
163+
limited_queryset = queryset.select_related("addon", "user").only(
164+
"addon_id",
165+
"user_id",
166+
"run_id",
167+
"status",
168+
"rating",
169+
"credits_spent",
170+
"created_at",
171+
"updated_at",
172+
"addon__name",
173+
"user__name",
174+
"user__email",
175+
)
176+
177+
for run in limited_queryset.iterator(chunk_size=settings.CSV_EXPORT_CHUNK_SIZE):
147178
writer.writerow(
148179
[
149180
run.addon_id,
181+
run.addon.name,
150182
run.user_id,
183+
run.user.name,
184+
run.user.email,
151185
run.run_id,
152186
run.status,
153187
run.rating,

documentcloud/documents/models/document.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,9 +702,14 @@ def page_filter(text):
702702
return solr_document
703703

704704
def invalidate_cache(self):
705-
"""Invalidate public CDN cache for this document's underlying file"""
705+
"""
706+
Invalidate public CDN cache for this document's underlying file,
707+
plus frontend URLs in Cloudflare
708+
"""
706709
logger.info("Invalidating cache for %s", self.pk)
707710
doc_path = self.doc_path[self.doc_path.index("/") :]
711+
712+
# cloudfront
708713
distribution_id = settings.CLOUDFRONT_DISTRIBUTION_ID
709714
if distribution_id:
710715
# we want the doc path without the s3 bucket name
@@ -716,15 +721,25 @@ def invalidate_cache(self):
716721
"CallerReference": str(uuid.uuid4()),
717722
},
718723
)
724+
725+
# cloudflare
719726
cloudflare_email = settings.CLOUDFLARE_API_EMAIL
720727
cloudflare_key = settings.CLOUDFLARE_API_KEY
721728
cloudflare_zone = settings.CLOUDFLARE_API_ZONE
722-
url = settings.PUBLIC_ASSET_URL + doc_path[1:]
729+
asset_url = settings.PUBLIC_ASSET_URL + doc_path[1:]
730+
731+
if self.access == Access.public:
732+
public_urls = [
733+
host + self.get_absolute_url() for host in settings.CLOUDFLARE_HOSTS
734+
] + [asset_url]
735+
else:
736+
public_urls = [asset_url]
737+
723738
if cloudflare_zone:
724739
requests.post(
725740
"https://api.cloudflare.com/client/v4/zones/"
726741
f"{cloudflare_zone}/purge_cache",
727-
json={"files": [url]},
742+
json={"files": public_urls},
728743
headers={
729744
"X-Auth-Email": cloudflare_email,
730745
"X-Auth-Key": cloudflare_key,

documentcloud/documents/oembed.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def response(self, request, query, max_width=None, max_height=None, **kwargs):
166166
context = {
167167
"src": src,
168168
"title": note.title,
169-
"style": self.get_style(document, note, max_width, max_height),
169+
"style": self.get_style(max_width, max_height),
170170
"width": note_width,
171171
"height": note_height,
172172
"resize_script": RESIZE_SCRIPT,
@@ -183,13 +183,11 @@ def get_dimensions(self, document, note):
183183

184184
return (note_width, note_height)
185185

186-
def get_style(self, document, note, max_width=None, max_height=None):
187-
188-
note_width, note_height = self.get_dimensions(document, note)
186+
def get_style(self, max_width=None, max_height=None):
189187

190188
style = (
191-
f"border: 1px solid #d8dee2; border-radius: 0.5rem; width: 100%;"
192-
f" height: 100%; aspect-ratio: {note_width} / {note_height};"
189+
"border: 1px solid #d8dee2; border-radius: 0.5rem;"
190+
" width: 100%; height: 300px;"
193191
)
194192
if max_width:
195193
style += f" max-width: {max_width}px;"

documentcloud/documents/tests/test_oembed.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,6 @@ def test_note_oembed_response(self):
354354
response = self.note_oembed.response(
355355
request, query, max_width=600, max_height=None, doc_pk=123, pk=456
356356
)
357-
note_width, note_height = self.note_oembed.get_dimensions(
358-
self.document, self.note
359-
)
360357

361358
# Check response properties
362359
assert response["version"] == "1.0"
@@ -370,10 +367,9 @@ def test_note_oembed_response(self):
370367
"?embed=1&responsive=1"
371368
) in response["html"]
372369
assert 'title="Test Note (Hosted by DocumentCloud)"' in response["html"]
373-
assert f'width="{note_width}" height="{note_height}"' in response["html"]
374370
assert (
375-
f"border: 1px solid #d8dee2; border-radius: 0.5rem; width: 100%;"
376-
f" height: 100%; aspect-ratio: {note_width} / {note_height};"
371+
"border: 1px solid #d8dee2; border-radius: 0.5rem;"
372+
" width: 100%; height: 300px;"
377373
) in response["html"]
378374
assert 'sandbox="allow-scripts allow-same-origin' in response["html"]
379375

documentcloud/organizations/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Django
2+
from django.conf import settings
23
from django.contrib import admin
34
from django.http import HttpResponse
45

@@ -41,7 +42,7 @@ def export_ai_credit_logs(self, request, queryset):
4142
writer = csv.writer(response)
4243
writer.writerow(field_names)
4344

44-
for log in queryset:
45+
for log in queryset.iterator(chunk_size=settings.CSV_EXPORT_CHUNK_SIZE):
4546
writer.writerow(
4647
[
4748
str(log.organization),

0 commit comments

Comments
 (0)