Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4CE DUA, institutional officials/members; PDF agreement forms #695

Merged
merged 5 commits into from
Aug 21, 2024
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
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Set arch
ARG BUILD_ARCH=amd64

FROM hmsdbmitc/dbmisvc:debian12-slim-python3.11-0.6.2 AS builder

ARG BUILD_ARCH

# Install requirements
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
Expand All @@ -10,8 +15,12 @@ RUN apt-get update \
default-libmysqlclient-dev \
libssl-dev \
pkg-config \
libfontconfig \
&& rm -rf /var/lib/apt/lists/*

# Install requirements for PDF generation
ADD phantomjs-2.1.1-${BUILD_ARCH}.tar.gz /tmp/

# Add requirements
ADD requirements.* /

Expand All @@ -27,6 +36,7 @@ ARG APP_CODENAME="hypatio"
ARG VERSION
ARG COMMIT
ARG DATE
ARG BUILD_ARCH

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

# Copy PhantomJS binary
COPY --from=builder /tmp/phantomjs /usr/local/bin/phantomjs

# Copy Python wheels from builder
COPY --from=builder /root/wheels /root/wheels

# Install requirements
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libfontconfig \
default-libmysqlclient-dev \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*
Expand Down
40 changes: 10 additions & 30 deletions app/hypatio/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
'django_jsonfield_backport',
'django_q',
'django_ses',
'pdf',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -149,7 +150,9 @@
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_S3_SIGNATURE_VERSION = 's3v4'
AWS_STORAGE_BUCKET_NAME = environment.get_str('S3_BUCKET', required=True)
AWS_LOCATION = 'upload'

PROJECTS_UPLOADS_PREFIX = "upload"
PROJECTS_DOCUMENTS_PREFIX = "documents"

##########

Expand Down Expand Up @@ -316,35 +319,12 @@
)

# Output the standard logging configuration
LOGGING = config('HYPATIO', root_level=logging.DEBUG)

# Disable warning level for 4xx request logging
LOGGING['loggers'].update({
'django.request': {
'handlers': ['console'],
'level': 'ERROR',
'propagate': True,
},
'boto3': {
'handlers': ['console'],
'level': 'INFO',
'propagate': True,
},
'botocore': {
'handlers': ['console'],
'level': 'INFO',
'propagate': True,
},
's3transfer': {
'handlers': ['console'],
'level': 'INFO',
'propagate': True,
},
'urllib3': {
'handlers': ['console'],
'level': 'INFO',
'propagate': True,
},
LOGGING = config('HYPATIO', root_level=logging.DEBUG, logger_levels={
"django.request": "ERROR",
"boto3": "INFO",
"botocore": "INFO",
"s3transfer": "INFO",
"urllib3": "INFO",
})

#####################################################################################
19 changes: 19 additions & 0 deletions app/pdf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
""" ______ __
____ ____/ / __/ ____ ____ ____ ___ _________ _/ /_____ _____
/ __ \/ __ / /_ / __ `/ _ \/ __ \/ _ \/ ___/ __ `/ __/ __ \/ ___/
/ /_/ / /_/ / __/ / /_/ / __/ / / / __/ / / /_/ / /_/ /_/ / /
/ .___/\__,_/_/_____\__, /\___/_/ /_/\___/_/ \__,_/\__/\____/_/
/_/ /_____/____/
"""

__title__ = 'PDF Generator'
__version__ = '0.1.3'
__author__ = 'Charles TISSIER'
__license__ = 'MIT'
__copyright__ = 'Copyright 2017 Charles TISSIER'

# Version synonym
VERSION = __version__


default_app_config = 'pdf.apps.PdfGeneratorConfig'
7 changes: 7 additions & 0 deletions app/pdf/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import unicode_literals

from django.apps import AppConfig


class PdfGeneratorConfig(AppConfig):
name = 'pdf'
80 changes: 80 additions & 0 deletions app/pdf/generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import subprocess
import os
import random

from .settings import pdf_settings
from django.http import HttpResponse
from django.core.files.base import ContentFile


class PDFGenerator(object):
def __init__(self, html, paperformat='A4', zoom=1, script=pdf_settings.DEFAULT_RASTERIZE_SCRIPT,
temp_dir=pdf_settings.DEFAULT_TEMP_DIR):
self.script = script
self.temp_dir = temp_dir
self.html = html
self.html_file = self.__get_html_filepath()
self.pdf_file = self.__get_pdf_filepath()
self.paperformat = paperformat
self.zoom = zoom
self.pdf_data = None

self.__write_html()
self.__generate()
self.__set_pdf_data()
self.__remove_source_file()

def __write_html(self):
with open(self.html_file, 'w') as f:
f.write(self.html)
f.close()

def __get_html_filepath(self):
return os.path.join(self.temp_dir, '{}.html'.format(PDFGenerator.get_random_filename(25)))

def __get_pdf_filepath(self):
return os.path.join(self.temp_dir, '{}.pdf'.format(PDFGenerator.get_random_filename(25)))

def __generate(self):
"""
call the following command:
phantomjs rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]
"""
phantomjs_env = os.environ.copy()
phantomjs_env["OPENSSL_CONF"] = "/etc/openssl/"
command = [
pdf_settings.PHANTOMJS_BIN_PATH,
'--ssl-protocol=any',
'--ignore-ssl-errors=yes',
self.script,
self.html_file,
self.pdf_file,
self.paperformat,
str(self.zoom)
]
return subprocess.call(command, env=phantomjs_env)

def __set_pdf_data(self):
with open(self.pdf_file, "rb") as pdf:
self.pdf_data = pdf.read()

def get_content_file(self, filename):
return ContentFile(self.pdf_data, name=filename)

def get_data(self):
return self.pdf_data

def get_http_response(self, filename):
response = HttpResponse(self.pdf_data, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(filename)
return response

def __remove_source_file(self):
html_rm = subprocess.call(['rm', self.html_file])
pdf_rm = subprocess.call(['rm', self.pdf_file])
return html_rm & pdf_rm

@staticmethod
def get_random_filename(nb=50):
choices = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return "".join([random.choice(choices) for _ in range(nb)])
50 changes: 50 additions & 0 deletions app/pdf/rasterize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js
"use strict";
var page = require('webpage').create(),
system = require('system'),
address, output, size, pageWidth, pageHeight;

if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px');
console.log(' "800px*600px" window, clipped to 800x600');
phantom.exit(1);
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = { width: 600, height: 600 };
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*');
page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' }
: { format: system.args[3], orientation: 'portrait', margin: '1.5cm' };
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
size = system.args[3].split('*');
if (size.length === 2) {
pageWidth = parseInt(size[0], 10);
pageHeight = parseInt(size[1], 10);
page.viewportSize = { width: pageWidth, height: pageHeight };
page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight };
} else {
console.log("size:", system.args[3]);
pageWidth = parseInt(system.args[3], 10);
pageHeight = parseInt(pageWidth * 3/4, 10); // it's as good an assumption as any
console.log ("pageHeight:",pageHeight);
page.viewportSize = { width: pageWidth, height: pageHeight };
}
}
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
window.setTimeout(function () {
page.render(output);
phantom.exit();
}, 200);
}
});
}
12 changes: 12 additions & 0 deletions app/pdf/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.template import loader

from .generators import PDFGenerator


def render_pdf(filename, request, template_name, context=None, using=None, options={}):

# Render to file.
content = loader.render_to_string(template_name, context, request, using=using)
pdf = PDFGenerator(content, **options)

return pdf.get_http_response(filename)
74 changes: 74 additions & 0 deletions app/pdf/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
Settings for PDF Generator are all namespaced in the PDF_GENERATOR setting.
For example your project's `settings.py` file might look like this:

PDF_GENERATOR = {
'UPLOAD_TO': 'pdfs',
}

This module provides the `pdf_setting` object, that is used to access
PDF settings, checking for user settings first, then falling
back to the defaults.
"""
from __future__ import unicode_literals
import os
from django.conf import settings
from django.test.signals import setting_changed


PDF_GENERATOR_DIR = os.path.dirname(os.path.abspath(__file__))

DEFAULTS = {
'UPLOAD_TO': 'pdfs',
'PHANTOMJS_BIN_PATH': 'phantomjs',
'DEFAULT_RASTERIZE_SCRIPT': os.path.join(PDF_GENERATOR_DIR, 'rasterize.js'),
'DEFAULT_TEMP_DIR': os.path.join(PDF_GENERATOR_DIR, 'temp'),
'TEMPLATES_DIR': os.path.join(PDF_GENERATOR_DIR, 'templates/pdf_generator')
}


class PDFSettings(object):
"""
A settings object, that allows PDF settings to be accessed as properties.
For example:
from pdf_generator.settings import api_settings
print(pdf_settings.UPLOAD_TO)
"""
def __init__(self, user_settings=None, defaults=None):
if user_settings:
self._user_settings = user_settings
self.defaults = defaults or DEFAULTS

@property
def user_settings(self):
if not hasattr(self, '_user_settings'):
self._user_settings = getattr(settings, 'PDF_GENERATOR', {})
return self._user_settings

def __getattr__(self, attr):
if attr not in self.defaults:
raise AttributeError("Invalid PDF Generator setting: '%s'" % attr)

try:
# Check if present in user settings
val = self.user_settings[attr]
except KeyError:
# Fall back to defaults
val = self.defaults[attr]

# Cache the result
setattr(self, attr, val)
return val


pdf_settings = PDFSettings(None, DEFAULTS)


def reload_pdf_settings(*args, **kwargs):
global pdf_settings
setting, value = kwargs['setting'], kwargs['value']
if setting == 'PDF_GENERATOR':
pdf_settings = PDFSettings(value, DEFAULTS)


setting_changed.connect(reload_pdf_settings)
Empty file added app/pdf/temp/empty.txt
Empty file.
3 changes: 3 additions & 0 deletions app/pdf/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
12 changes: 12 additions & 0 deletions app/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from projects.models import ChallengeTaskSubmission
from projects.models import ChallengeTaskSubmissionDownload
from projects.models import Bucket
from projects.models import InstitutionalOfficial
from projects.models import InstitutionalMember


class GroupAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -58,6 +60,14 @@ class InstitutionAdmin(admin.ModelAdmin):
list_display = ('name', 'logo_path', 'created', 'modified', )
readonly_fields = ('created', 'modified', )

class InstitutionalOfficialAdmin(admin.ModelAdmin):
list_display = ('user', 'institution', 'project', 'created', 'modified', )
readonly_fields = ('created', 'modified', )

class InstitutionalMemberAdmin(admin.ModelAdmin):
list_display = ('email', 'official', 'user', 'created', 'modified', )
readonly_fields = ('created', 'modified', )

class HostedFileAdmin(admin.ModelAdmin):
list_display = ('long_name', 'project', 'hostedfileset', 'file_name', 'file_location', 'order', 'created', 'modified',)
list_filter = ('project', )
Expand Down Expand Up @@ -95,6 +105,8 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin):
admin.site.register(Team, TeamAdmin)
admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Institution, InstitutionAdmin)
admin.site.register(InstitutionalOfficial, InstitutionalOfficialAdmin)
admin.site.register(InstitutionalMember, InstitutionalMemberAdmin)
admin.site.register(HostedFile, HostedFileAdmin)
admin.site.register(HostedFileSet, HostedFileSetAdmin)
admin.site.register(HostedFileDownload, HostedFileDownloadAdmin)
Expand Down
Loading