Skip to content

Commit 78c7cb5

Browse files
authored
Merge pull request #695 from hms-dbmi/feature-4ce-dua
4CE DUA, institutional officials/members; PDF agreement forms
2 parents 03bd2d0 + bfe2da4 commit 78c7cb5

34 files changed

+1502
-274
lines changed

Diff for: Dockerfile

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
# Set arch
2+
ARG BUILD_ARCH=amd64
3+
14
FROM hmsdbmitc/dbmisvc:debian12-slim-python3.11-0.6.2 AS builder
25

6+
ARG BUILD_ARCH
7+
38
# Install requirements
49
RUN apt-get update \
510
&& apt-get install -y --no-install-recommends \
@@ -10,8 +15,12 @@ RUN apt-get update \
1015
default-libmysqlclient-dev \
1116
libssl-dev \
1217
pkg-config \
18+
libfontconfig \
1319
&& rm -rf /var/lib/apt/lists/*
1420

21+
# Install requirements for PDF generation
22+
ADD phantomjs-2.1.1-${BUILD_ARCH}.tar.gz /tmp/
23+
1524
# Add requirements
1625
ADD requirements.* /
1726

@@ -27,6 +36,7 @@ ARG APP_CODENAME="hypatio"
2736
ARG VERSION
2837
ARG COMMIT
2938
ARG DATE
39+
ARG BUILD_ARCH
3040

3141
LABEL org.label-schema.schema-version=1.0 \
3242
org.label-schema.vendor="HMS-DBMI" \
@@ -38,12 +48,16 @@ LABEL org.label-schema.schema-version=1.0 \
3848
org.label-schema.vcs-url="https://github.com/hms-dbmi/hypatio-app" \
3949
org.label-schema.vcf-ref=${COMMIT}
4050

51+
# Copy PhantomJS binary
52+
COPY --from=builder /tmp/phantomjs /usr/local/bin/phantomjs
53+
4154
# Copy Python wheels from builder
4255
COPY --from=builder /root/wheels /root/wheels
4356

4457
# Install requirements
4558
RUN apt-get update \
4659
&& apt-get install -y --no-install-recommends \
60+
libfontconfig \
4761
default-libmysqlclient-dev \
4862
libmagic1 \
4963
&& rm -rf /var/lib/apt/lists/*

Diff for: app/hypatio/settings.py

+10-30
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
'django_jsonfield_backport',
6060
'django_q',
6161
'django_ses',
62+
'pdf',
6263
]
6364

6465
MIDDLEWARE = [
@@ -149,7 +150,9 @@
149150
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
150151
AWS_S3_SIGNATURE_VERSION = 's3v4'
151152
AWS_STORAGE_BUCKET_NAME = environment.get_str('S3_BUCKET', required=True)
152-
AWS_LOCATION = 'upload'
153+
154+
PROJECTS_UPLOADS_PREFIX = "upload"
155+
PROJECTS_DOCUMENTS_PREFIX = "documents"
153156

154157
##########
155158

@@ -316,35 +319,12 @@
316319
)
317320

318321
# Output the standard logging configuration
319-
LOGGING = config('HYPATIO', root_level=logging.DEBUG)
320-
321-
# Disable warning level for 4xx request logging
322-
LOGGING['loggers'].update({
323-
'django.request': {
324-
'handlers': ['console'],
325-
'level': 'ERROR',
326-
'propagate': True,
327-
},
328-
'boto3': {
329-
'handlers': ['console'],
330-
'level': 'INFO',
331-
'propagate': True,
332-
},
333-
'botocore': {
334-
'handlers': ['console'],
335-
'level': 'INFO',
336-
'propagate': True,
337-
},
338-
's3transfer': {
339-
'handlers': ['console'],
340-
'level': 'INFO',
341-
'propagate': True,
342-
},
343-
'urllib3': {
344-
'handlers': ['console'],
345-
'level': 'INFO',
346-
'propagate': True,
347-
},
322+
LOGGING = config('HYPATIO', root_level=logging.DEBUG, logger_levels={
323+
"django.request": "ERROR",
324+
"boto3": "INFO",
325+
"botocore": "INFO",
326+
"s3transfer": "INFO",
327+
"urllib3": "INFO",
348328
})
349329

350330
#####################################################################################

Diff for: app/pdf/__init__.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
""" ______ __
2+
____ ____/ / __/ ____ ____ ____ ___ _________ _/ /_____ _____
3+
/ __ \/ __ / /_ / __ `/ _ \/ __ \/ _ \/ ___/ __ `/ __/ __ \/ ___/
4+
/ /_/ / /_/ / __/ / /_/ / __/ / / / __/ / / /_/ / /_/ /_/ / /
5+
/ .___/\__,_/_/_____\__, /\___/_/ /_/\___/_/ \__,_/\__/\____/_/
6+
/_/ /_____/____/
7+
"""
8+
9+
__title__ = 'PDF Generator'
10+
__version__ = '0.1.3'
11+
__author__ = 'Charles TISSIER'
12+
__license__ = 'MIT'
13+
__copyright__ = 'Copyright 2017 Charles TISSIER'
14+
15+
# Version synonym
16+
VERSION = __version__
17+
18+
19+
default_app_config = 'pdf.apps.PdfGeneratorConfig'

Diff for: app/pdf/apps.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from __future__ import unicode_literals
2+
3+
from django.apps import AppConfig
4+
5+
6+
class PdfGeneratorConfig(AppConfig):
7+
name = 'pdf'

Diff for: app/pdf/generators.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import subprocess
2+
import os
3+
import random
4+
5+
from .settings import pdf_settings
6+
from django.http import HttpResponse
7+
from django.core.files.base import ContentFile
8+
9+
10+
class PDFGenerator(object):
11+
def __init__(self, html, paperformat='A4', zoom=1, script=pdf_settings.DEFAULT_RASTERIZE_SCRIPT,
12+
temp_dir=pdf_settings.DEFAULT_TEMP_DIR):
13+
self.script = script
14+
self.temp_dir = temp_dir
15+
self.html = html
16+
self.html_file = self.__get_html_filepath()
17+
self.pdf_file = self.__get_pdf_filepath()
18+
self.paperformat = paperformat
19+
self.zoom = zoom
20+
self.pdf_data = None
21+
22+
self.__write_html()
23+
self.__generate()
24+
self.__set_pdf_data()
25+
self.__remove_source_file()
26+
27+
def __write_html(self):
28+
with open(self.html_file, 'w') as f:
29+
f.write(self.html)
30+
f.close()
31+
32+
def __get_html_filepath(self):
33+
return os.path.join(self.temp_dir, '{}.html'.format(PDFGenerator.get_random_filename(25)))
34+
35+
def __get_pdf_filepath(self):
36+
return os.path.join(self.temp_dir, '{}.pdf'.format(PDFGenerator.get_random_filename(25)))
37+
38+
def __generate(self):
39+
"""
40+
call the following command:
41+
phantomjs rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]
42+
"""
43+
phantomjs_env = os.environ.copy()
44+
phantomjs_env["OPENSSL_CONF"] = "/etc/openssl/"
45+
command = [
46+
pdf_settings.PHANTOMJS_BIN_PATH,
47+
'--ssl-protocol=any',
48+
'--ignore-ssl-errors=yes',
49+
self.script,
50+
self.html_file,
51+
self.pdf_file,
52+
self.paperformat,
53+
str(self.zoom)
54+
]
55+
return subprocess.call(command, env=phantomjs_env)
56+
57+
def __set_pdf_data(self):
58+
with open(self.pdf_file, "rb") as pdf:
59+
self.pdf_data = pdf.read()
60+
61+
def get_content_file(self, filename):
62+
return ContentFile(self.pdf_data, name=filename)
63+
64+
def get_data(self):
65+
return self.pdf_data
66+
67+
def get_http_response(self, filename):
68+
response = HttpResponse(self.pdf_data, content_type='application/pdf')
69+
response['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(filename)
70+
return response
71+
72+
def __remove_source_file(self):
73+
html_rm = subprocess.call(['rm', self.html_file])
74+
pdf_rm = subprocess.call(['rm', self.pdf_file])
75+
return html_rm & pdf_rm
76+
77+
@staticmethod
78+
def get_random_filename(nb=50):
79+
choices = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
80+
return "".join([random.choice(choices) for _ in range(nb)])

Diff for: app/pdf/rasterize.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js
2+
"use strict";
3+
var page = require('webpage').create(),
4+
system = require('system'),
5+
address, output, size, pageWidth, pageHeight;
6+
7+
if (system.args.length < 3 || system.args.length > 5) {
8+
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
9+
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
10+
console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px');
11+
console.log(' "800px*600px" window, clipped to 800x600');
12+
phantom.exit(1);
13+
} else {
14+
address = system.args[1];
15+
output = system.args[2];
16+
page.viewportSize = { width: 600, height: 600 };
17+
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
18+
size = system.args[3].split('*');
19+
page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' }
20+
: { format: system.args[3], orientation: 'portrait', margin: '1.5cm' };
21+
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
22+
size = system.args[3].split('*');
23+
if (size.length === 2) {
24+
pageWidth = parseInt(size[0], 10);
25+
pageHeight = parseInt(size[1], 10);
26+
page.viewportSize = { width: pageWidth, height: pageHeight };
27+
page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight };
28+
} else {
29+
console.log("size:", system.args[3]);
30+
pageWidth = parseInt(system.args[3], 10);
31+
pageHeight = parseInt(pageWidth * 3/4, 10); // it's as good an assumption as any
32+
console.log ("pageHeight:",pageHeight);
33+
page.viewportSize = { width: pageWidth, height: pageHeight };
34+
}
35+
}
36+
if (system.args.length > 4) {
37+
page.zoomFactor = system.args[4];
38+
}
39+
page.open(address, function (status) {
40+
if (status !== 'success') {
41+
console.log('Unable to load the address!');
42+
phantom.exit(1);
43+
} else {
44+
window.setTimeout(function () {
45+
page.render(output);
46+
phantom.exit();
47+
}, 200);
48+
}
49+
});
50+
}

Diff for: app/pdf/renderers.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.template import loader
2+
3+
from .generators import PDFGenerator
4+
5+
6+
def render_pdf(filename, request, template_name, context=None, using=None, options={}):
7+
8+
# Render to file.
9+
content = loader.render_to_string(template_name, context, request, using=using)
10+
pdf = PDFGenerator(content, **options)
11+
12+
return pdf.get_http_response(filename)

Diff for: app/pdf/settings.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Settings for PDF Generator are all namespaced in the PDF_GENERATOR setting.
3+
For example your project's `settings.py` file might look like this:
4+
5+
PDF_GENERATOR = {
6+
'UPLOAD_TO': 'pdfs',
7+
}
8+
9+
This module provides the `pdf_setting` object, that is used to access
10+
PDF settings, checking for user settings first, then falling
11+
back to the defaults.
12+
"""
13+
from __future__ import unicode_literals
14+
import os
15+
from django.conf import settings
16+
from django.test.signals import setting_changed
17+
18+
19+
PDF_GENERATOR_DIR = os.path.dirname(os.path.abspath(__file__))
20+
21+
DEFAULTS = {
22+
'UPLOAD_TO': 'pdfs',
23+
'PHANTOMJS_BIN_PATH': 'phantomjs',
24+
'DEFAULT_RASTERIZE_SCRIPT': os.path.join(PDF_GENERATOR_DIR, 'rasterize.js'),
25+
'DEFAULT_TEMP_DIR': os.path.join(PDF_GENERATOR_DIR, 'temp'),
26+
'TEMPLATES_DIR': os.path.join(PDF_GENERATOR_DIR, 'templates/pdf_generator')
27+
}
28+
29+
30+
class PDFSettings(object):
31+
"""
32+
A settings object, that allows PDF settings to be accessed as properties.
33+
For example:
34+
from pdf_generator.settings import api_settings
35+
print(pdf_settings.UPLOAD_TO)
36+
"""
37+
def __init__(self, user_settings=None, defaults=None):
38+
if user_settings:
39+
self._user_settings = user_settings
40+
self.defaults = defaults or DEFAULTS
41+
42+
@property
43+
def user_settings(self):
44+
if not hasattr(self, '_user_settings'):
45+
self._user_settings = getattr(settings, 'PDF_GENERATOR', {})
46+
return self._user_settings
47+
48+
def __getattr__(self, attr):
49+
if attr not in self.defaults:
50+
raise AttributeError("Invalid PDF Generator setting: '%s'" % attr)
51+
52+
try:
53+
# Check if present in user settings
54+
val = self.user_settings[attr]
55+
except KeyError:
56+
# Fall back to defaults
57+
val = self.defaults[attr]
58+
59+
# Cache the result
60+
setattr(self, attr, val)
61+
return val
62+
63+
64+
pdf_settings = PDFSettings(None, DEFAULTS)
65+
66+
67+
def reload_pdf_settings(*args, **kwargs):
68+
global pdf_settings
69+
setting, value = kwargs['setting'], kwargs['value']
70+
if setting == 'PDF_GENERATOR':
71+
pdf_settings = PDFSettings(value, DEFAULTS)
72+
73+
74+
setting_changed.connect(reload_pdf_settings)

Diff for: app/pdf/temp/empty.txt

Whitespace-only changes.

Diff for: app/pdf/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.test import TestCase
2+
3+
# Create your tests here.

Diff for: app/projects/admin.py

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from projects.models import ChallengeTaskSubmission
1717
from projects.models import ChallengeTaskSubmissionDownload
1818
from projects.models import Bucket
19+
from projects.models import InstitutionalOfficial
20+
from projects.models import InstitutionalMember
1921

2022

2123
class GroupAdmin(admin.ModelAdmin):
@@ -58,6 +60,14 @@ class InstitutionAdmin(admin.ModelAdmin):
5860
list_display = ('name', 'logo_path', 'created', 'modified', )
5961
readonly_fields = ('created', 'modified', )
6062

63+
class InstitutionalOfficialAdmin(admin.ModelAdmin):
64+
list_display = ('user', 'institution', 'project', 'created', 'modified', )
65+
readonly_fields = ('created', 'modified', )
66+
67+
class InstitutionalMemberAdmin(admin.ModelAdmin):
68+
list_display = ('email', 'official', 'user', 'created', 'modified', )
69+
readonly_fields = ('created', 'modified', )
70+
6171
class HostedFileAdmin(admin.ModelAdmin):
6272
list_display = ('long_name', 'project', 'hostedfileset', 'file_name', 'file_location', 'order', 'created', 'modified',)
6373
list_filter = ('project', )
@@ -95,6 +105,8 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin):
95105
admin.site.register(Team, TeamAdmin)
96106
admin.site.register(Participant, ParticipantAdmin)
97107
admin.site.register(Institution, InstitutionAdmin)
108+
admin.site.register(InstitutionalOfficial, InstitutionalOfficialAdmin)
109+
admin.site.register(InstitutionalMember, InstitutionalMemberAdmin)
98110
admin.site.register(HostedFile, HostedFileAdmin)
99111
admin.site.register(HostedFileSet, HostedFileSetAdmin)
100112
admin.site.register(HostedFileDownload, HostedFileDownloadAdmin)

0 commit comments

Comments
 (0)