diff --git a/Ion.egg-info/PKG-INFO b/Ion.egg-info/PKG-INFO
index 6524fd61927..c18a45dad4d 100644
--- a/Ion.egg-info/PKG-INFO
+++ b/Ion.egg-info/PKG-INFO
@@ -11,6 +11,71 @@ Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.8
Classifier: Framework :: Django :: 3.2
License-File: COPYING
+Requires-Dist: argon2-cffi==21.3.0
+Requires-Dist: autobahn==22.7.1
+Requires-Dist: autopep8==1.7.0
+Requires-Dist: Babel==2.10.3
+Requires-Dist: bcrypt==4.0.0
+Requires-Dist: beautifulsoup4==4.11.1
+Requires-Dist: black==23.1.0
+Requires-Dist: bleach==5.0.1
+Requires-Dist: celery==5.2.7
+Requires-Dist: certifi==2022.12.7
+Requires-Dist: channels==3.0.5
+Requires-Dist: channels-redis==3.4.1
+Requires-Dist: contextlib2==21.6.0
+Requires-Dist: cryptography==41.0.0
+Requires-Dist: decorator==5.1.1
+Requires-Dist: Django==3.2.20
+Requires-Dist: django-cacheops==7.0.1
+Requires-Dist: django-cors-headers==3.13.0
+Requires-Dist: django-debug-toolbar==3.6.0
+Requires-Dist: django-extensions==3.2.0
+Requires-Dist: django-formtools==2.3
+Requires-Dist: django-inline-svg==0.1.1
+Requires-Dist: django-maintenance-mode==0.16.3
+Requires-Dist: django-oauth-toolkit==2.1.0
+Requires-Dist: django-pipeline==2.0.9
+Requires-Dist: django-prometheus==2.2.0
+Requires-Dist: django-redis-cache==3.0.1
+Requires-Dist: django-redis-sessions==0.6.2
+Requires-Dist: django-referrer-policy==1.0
+Requires-Dist: django-requestlogging-redux==1.2.1
+Requires-Dist: django-simple-history==3.1.1
+Requires-Dist: django-user-agents==0.4.0
+Requires-Dist: django-widget-tweaks==1.4.12
+Requires-Dist: djangorestframework==3.14.0
+Requires-Dist: docutils==0.19
+Requires-Dist: Fabric3==1.14.post1
+Requires-Dist: flower==1.2.0
+Requires-Dist: gunicorn==20.1.0
+Requires-Dist: hiredis==2.0.0
+Requires-Dist: ipython==8.10.0
+Requires-Dist: isort==5.12.0
+Requires-Dist: lxml==4.9.1
+Requires-Dist: objgraph==3.5.0
+Requires-Dist: pexpect==4.8.0
+Requires-Dist: prometheus-client==0.17.0
+Requires-Dist: psycopg2==2.9.3
+Requires-Dist: pycryptodome==3.18.0
+Requires-Dist: pyrankvote==2.0.5
+Requires-Dist: pysftp==0.2.9
+Requires-Dist: python-dateutil==2.8.2
+Requires-Dist: python-magic==0.4.27
+Requires-Dist: reportlab==3.6.11
+Requires-Dist: requests==2.31.0
+Requires-Dist: requests-oauthlib==1.3.1
+Requires-Dist: sentry-sdk==1.15.0
+Requires-Dist: setuptools-git==1.2
+Requires-Dist: Sphinx==5.2.3
+Requires-Dist: sphinx-bootstrap-theme==0.8.1
+Requires-Dist: tblib==1.7.0
+Requires-Dist: vine==5.0.0
+Requires-Dist: xhtml2pdf==0.2.11
+Requires-Dist: asgiref>=3.3.4
+Requires-Dist: pillow>=9.0.0
+Requires-Dist: tinycss2
+Requires-Dist: twisted>=21.7.0
**********
Intranet 3
diff --git a/Ion.egg-info/SOURCES.txt b/Ion.egg-info/SOURCES.txt
index 17b9a834f69..abd79e19cee 100644
--- a/Ion.egg-info/SOURCES.txt
+++ b/Ion.egg-info/SOURCES.txt
@@ -824,6 +824,7 @@ intranet/apps/printing/migrations/0006_printjob_page_range.py
intranet/apps/printing/migrations/0007_printjob_duplex.py
intranet/apps/printing/migrations/0008_auto_20160828_2058.py
intranet/apps/printing/migrations/0009_printjob_fit.py
+intranet/apps/printing/migrations/0010_alter_printjob_duplex.py
intranet/apps/printing/migrations/__init__.py
intranet/apps/schedule/__init__.py
intranet/apps/schedule/admin.py
@@ -3618,6 +3619,7 @@ intranet/templates/preferences/preferences.html
intranet/templates/preferences/privacy_options.html
intranet/templates/printing/print.html
intranet/templates/printing/print_form.html
+intranet/templates/printing/title_page.html
intranet/templates/rest_framework/api.html
intranet/templates/rest_framework/login.html
intranet/templates/schedule/admin_add.html
diff --git a/Ion.egg-info/requires.txt b/Ion.egg-info/requires.txt
index 3fe711ce8bf..99c2208d9f5 100644
--- a/Ion.egg-info/requires.txt
+++ b/Ion.egg-info/requires.txt
@@ -58,6 +58,7 @@ Sphinx==5.2.3
sphinx-bootstrap-theme==0.8.1
tblib==1.7.0
vine==5.0.0
+xhtml2pdf==0.2.11
asgiref>=3.3.4
pillow>=9.0.0
tinycss2
diff --git a/docs/rtd-requirements.txt b/docs/rtd-requirements.txt
index 5cc337f6a12..47927e92556 100644
--- a/docs/rtd-requirements.txt
+++ b/docs/rtd-requirements.txt
@@ -58,6 +58,7 @@ Sphinx==5.2.3
sphinx-bootstrap-theme==0.8.1
tblib==1.7.0
vine==5.0.0
+xhtml2pdf==0.2.11
# Not direct dependencies, but need to be bumped for some reason
# (for example, bug or security fixes)
diff --git a/intranet/apps/printing/migrations/0010_alter_printjob_duplex.py b/intranet/apps/printing/migrations/0010_alter_printjob_duplex.py
new file mode 100644
index 00000000000..9dbadbc63ac
--- /dev/null
+++ b/intranet/apps/printing/migrations/0010_alter_printjob_duplex.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.20 on 2023-11-02 01:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('printing', '0009_printjob_fit'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='printjob',
+ name='duplex',
+ field=models.BooleanField(default=True, verbose_name='Double-sided'),
+ ),
+ ]
diff --git a/intranet/apps/printing/models.py b/intranet/apps/printing/models.py
index d36ec040db5..641f38163d6 100644
--- a/intranet/apps/printing/models.py
+++ b/intranet/apps/printing/models.py
@@ -26,7 +26,7 @@ class PrintJob(models.Model):
time = models.DateTimeField(auto_now_add=True)
printed = models.BooleanField(default=False)
num_pages = models.IntegerField(default=0)
- duplex = models.BooleanField(default=True)
+ duplex = models.BooleanField(default=True, verbose_name="Double-sided")
fit = models.BooleanField(default=False, verbose_name="Fit-to-page")
def __str__(self):
diff --git a/intranet/apps/printing/views.py b/intranet/apps/printing/views.py
index 08a561436f3..0ca6783f15f 100644
--- a/intranet/apps/printing/views.py
+++ b/intranet/apps/printing/views.py
@@ -4,16 +4,20 @@
import re
import subprocess
import tempfile
+from io import BytesIO
from typing import Dict, Optional
import magic
from sentry_sdk import add_breadcrumb, capture_exception
+from xhtml2pdf import pisa
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.shortcuts import redirect, render
+from django.template.loader import get_template
+from django.utils import timezone
from django.utils.text import slugify
from ..auth.decorators import deny_restricted
@@ -248,6 +252,23 @@ def check_page_range(page_range: str, max_pages: int) -> Optional[int]:
return pages
+def html_to_pdf(template_src, filename, context=None):
+ if context is None:
+ context = {}
+ template = get_template(template_src)
+ html = template.render(context)
+ result = BytesIO()
+ pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
+ if not pdf.err:
+ filename_without_extension = os.path.basename(os.path.splitext(filename)[0])
+ filename = filename_without_extension + ".pdf"
+ title_tmpfile_fd, title_tmpfile_name = tempfile.mkstemp(prefix=f"ion_title_print_{filename}")
+ with open(title_tmpfile_fd, "wb") as f:
+ f.write(result.getvalue())
+ return title_tmpfile_name
+ return None
+
+
def print_job(obj: PrintJob, do_print: bool = True):
printer = obj.printer
if printer not in get_printers().keys():
@@ -352,14 +373,28 @@ def print_job(obj: PrintJob, do_print: bool = True):
if obj.fit:
args.extend(["-o", "fit-to-page"])
+ title_page = html_to_pdf(
+ "printing/title_page.html",
+ final_filename,
+ {
+ "obj": obj,
+ "filename": filebase,
+ "time": timezone.now(),
+ "pages": num_pages,
+ },
+ )
+
+ delete_filenames.add(title_page)
+
try:
+ subprocess.check_output(["lpr", "-P", printer, title_page], stderr=subprocess.STDOUT, universal_newlines=True)
subprocess.check_output(args, stderr=subprocess.STDOUT, universal_newlines=True)
except subprocess.CalledProcessError as e:
if "is not accepting jobs" in e.output:
raise Exception(e.output.strip()) from e
logger.error("Could not run lpr (returned %d): %s", e.returncode, e.output.strip())
- raise Exception("An error occured while printing your file: {}".format(e.output.strip())) from e
+ raise Exception("An error occurred while printing your file: {}".format(e.output.strip())) from e
obj.printed = True
obj.save()
@@ -394,9 +429,11 @@ def print_view(request):
messages.success(
request,
"Your file was submitted to the printer. "
- "If the printers are experiencing trouble, please contact the "
- "Student Systems Administrators by filling out the feedback "
- "form.",
+ "Do not re-print this job if it does not come out of the printer - "
+ "in nearly all cases, the job has been received and re-printing"
+ "will cause multiple copies to be printed."
+ "Ask for help instead by contacting the "
+ "Student Systems Administrators by filling out the feedback form.",
)
else:
form = PrintJobForm(printers=printers)
diff --git a/intranet/settings/__init__.py b/intranet/settings/__init__.py
index f1a64bb5287..70d9025ba16 100644
--- a/intranet/settings/__init__.py
+++ b/intranet/settings/__init__.py
@@ -210,7 +210,8 @@
# The maximum number of pages in one document that can be
# printed through the printing functionality (determined through pdfinfo)
-PRINTING_PAGES_LIMIT = 15
+# even number preferred to allow for maximum utilization of duplex printing
+PRINTING_PAGES_LIMIT = 16
# The maximum file upload and download size for files
FILES_MAX_UPLOAD_SIZE = 200 * 1024 * 1024
diff --git a/intranet/templates/printing/print_form.html b/intranet/templates/printing/print_form.html
index d595ee9cd93..fa479a42b12 100644
--- a/intranet/templates/printing/print_form.html
+++ b/intranet/templates/printing/print_form.html
@@ -22,9 +22,10 @@
The following restrictions apply:
Name: {{ obj.user.full_name }}
+Username: {{ obj.user.username }}
+ {% if obj.user.student_id %} +Student ID: {{ obj.user.student_id }}
+ {% endif %} +Time: {{ time }}
+Filename: {{ filename }}
+Pages: {{ pages }}
+ +Printer: {{ obj.printer }}
+Duplex: {{ obj.duplex }}
+Fit-to-page: {{ obj.fit }}
+ {% if obj.page_range %} +Page range: {{ obj.page_range }}
+ {% endif %} +