diff --git a/Dockerfile b/Dockerfile index 40b66527f..df675cde9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update \ cracklib-runtime \ libcrack2 \ libcrack2-dev \ + libcups2-dev \ libffi-dev \ libfreetype6-dev \ libpng-dev \ diff --git a/conf/ocfweb.conf b/conf/ocfweb.conf index a99bf20ff..b3b573f80 100644 --- a/conf/ocfweb.conf +++ b/conf/ocfweb.conf @@ -20,3 +20,7 @@ db = ocfmail user = ocfstats password = password db = ocfstats + +[printing] +# Joe's VM +otp_url = http://joe:15011 diff --git a/devenv.lock b/devenv.lock index 5916f2535..fdcda2431 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1777679510, + "lastModified": 1778692109, + "narHash": "sha256-uX572+AJ1TAXZDg+npJFq5LMGIGg9IzffhDqcUJsdA0=", "owner": "cachix", "repo": "devenv", - "rev": "bc8b21628907c726c74094cedc439c10a455cdb7", + "rev": "c733274dc2900f4bf8b3de279de8c5577930d982", "type": "github" }, "original": { @@ -16,71 +17,16 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1767039857, - "owner": "NixOS", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1776796298, - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762808025, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "nixpkgs": { "inputs": { "nixpkgs-src": "nixpkgs-src" }, "locked": { - "lastModified": 1776852779, + "lastModified": 1778507786, + "narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=", "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "ec3063523dcd911aeadb50faa589f237cdab5853", + "rev": "8f24a228a782e24576b155d1e39f0d914b380691", "type": "github" }, "original": { @@ -93,11 +39,11 @@ "nixpkgs-src": { "flake": false, "locked": { - "lastModified": 1776329215, - "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", + "lastModified": 1778274207, + "narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b86751bc4085f48661017fa226dee99fab6c651b", + "rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", "type": "github" }, "original": { @@ -110,11 +56,7 @@ "root": { "inputs": { "devenv": "devenv", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "git-hooks" - ] + "nixpkgs": "nixpkgs" } } }, diff --git a/devenv.nix b/devenv.nix index 70762f7b2..78a9276ff 100644 --- a/devenv.nix +++ b/devenv.nix @@ -12,6 +12,7 @@ uv.enable = true; }; packages = with pkgs; [ + cups gnumake libffi pkg-config diff --git a/ocfweb/account/print.py b/ocfweb/account/print.py new file mode 100644 index 000000000..d77383c56 --- /dev/null +++ b/ocfweb/account/print.py @@ -0,0 +1,118 @@ +import tempfile + +import cups +import requests +from django import forms +from django.conf import settings +from django.contrib import messages +from django.http import HttpRequest +from django.http import HttpResponse +from django.shortcuts import redirect +from django.shortcuts import render +from ocflib.printing.quota import get_connection as get_quota_connection +from ocflib.printing.quota import get_quota + +from ocfweb.auth import login_required +from ocfweb.component.forms import Form +from ocfweb.component.session import logged_in_user +from ocfweb.printing import get_printers + + +class WebPrintForm(Form): + printer = forms.ChoiceField( + label='Select Printer', + ) + file = forms.FileField( + label='File to Print', + ) + otp = forms.CharField( + label='Verification Code (From front of room)', + min_length=6, + max_length=6, + widget=forms.TextInput(attrs={'placeholder': '123456'}), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + printers = get_printers() + + choices = [] + for name, info in printers.items(): + if info.get('printer-is-shared', True): + choices.append((name, info.get('printer-info', name))) + + if not choices: + choices = [('', 'No printers available')] + + self.fields['printer'].choices = choices + + def clean_file(self): + return self.cleaned_data.get('file') + + def clean_otp(self): + otp = self.cleaned_data.get('otp') + if not otp: + return otp + + verify_url = f"{settings.PRINTING_OTP_URL}/verify/{otp}" + try: + response = requests.get(verify_url, timeout=5) + response.raise_for_status() + data = response.json() + if not data.get('valid'): + raise forms.ValidationError('Invalid code. Please get the code from the front of the room to enter.') + except (requests.RequestException, ValueError): + # Fail closed on server error + raise forms.ValidationError('Could not verify code with server. Please try again later.') + + return otp + + +@login_required +def web_print(request: HttpRequest) -> HttpResponse: + user = logged_in_user(request) + + with get_quota_connection() as c: + quota = get_quota(c, user) + + if request.method == 'POST': + form = WebPrintForm(request.POST, request.FILES) + if form.is_valid(): + printer = form.cleaned_data['printer'] + uploaded_file = form.cleaned_data['file'] + + try: + with tempfile.NamedTemporaryFile() as tmp: + for chunk in uploaded_file.chunks(): + tmp.write(chunk) + tmp.flush() + + # Submit to printhost CUPS + cups.setUser(user) + conn = cups.Connection(host='printhost') + conn.printFile( + printer, + tmp.name, + uploaded_file.name, + {}, + ) + + messages.success( + request, + f'Successfully submitted "{uploaded_file.name}" to {printer}.', + ) + return redirect('web_print') + except Exception as e: + messages.error(request, f"Failed to print: {e}") + else: + form = WebPrintForm() + + return render( + request, + 'account/print.html', + { + 'title': 'Web Printing', + 'form': form, + 'quota': quota, + }, + ) diff --git a/ocfweb/account/templates/account/print.html b/ocfweb/account/templates/account/print.html new file mode 100644 index 000000000..b336a3084 --- /dev/null +++ b/ocfweb/account/templates/account/print.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} +{% load bootstrap %} + +{% block content %} +
+
+

+ Welcome to web printing! You can upload a file here to print it to one of the lab printers. +

+ +
+ {% csrf_token %} +
+ {{ form|bootstrap }} +
+ +
+
+ +
+
+
+

Remaining Quota

+
+
+
+
Daily
+
{{ quota.daily }} pages
+
Semesterly
+
{{ quota.semesterly }} pages
+
+

+ Quota is deducted automatically when you print. + For more info, see the printing docs. +

+
+
+
+
+{% endblock %} diff --git a/ocfweb/account/urls.py b/ocfweb/account/urls.py index c6c363cd2..f39a349a8 100644 --- a/ocfweb/account/urls.py +++ b/ocfweb/account/urls.py @@ -2,6 +2,7 @@ from ocfweb.account.chpass import change_password from ocfweb.account.commands import commands +from ocfweb.account.print import web_print from ocfweb.account.register import account_created from ocfweb.account.register import account_pending from ocfweb.account.register import recommend @@ -19,6 +20,7 @@ urlpatterns = [ re_path(r'^password/$', change_password, name='change_password'), re_path(r'^commands/$', commands, name='commands'), + re_path(r'^print/$', web_print, name='web_print'), # account creation re_path(r'^register/$', request_account, name='register'), diff --git a/ocfweb/printing.py b/ocfweb/printing.py new file mode 100644 index 000000000..e6e8cbf08 --- /dev/null +++ b/ocfweb/printing.py @@ -0,0 +1,11 @@ +import cups + +from ocfweb.caching import cache + +@cache(ttl=60) +def get_printers(): + try: + conn = cups.Connection(host='printhost') + return conn.getPrinters() + except Exception: + return {} diff --git a/ocfweb/settings.py b/ocfweb/settings.py index cb042c89f..5660141b6 100644 --- a/ocfweb/settings.py +++ b/ocfweb/settings.py @@ -170,6 +170,8 @@ def __mod__(self, ref: Any) -> str: OCFSTATS_PASSWORD = conf.get('ocfstats', 'password') OCFSTATS_DB = conf.get('ocfstats', 'db') +PRINTING_OTP_URL = conf.get('printing', 'otp_url', fallback='http://127.0.0.1:8000') + if not DEBUG: # Prod-only settings. CACHES['default'] = { diff --git a/ocfweb/stats/printing.py b/ocfweb/stats/printing.py index 7e47282b4..461b05b3e 100644 --- a/ocfweb/stats/printing.py +++ b/ocfweb/stats/printing.py @@ -12,15 +12,15 @@ from django.shortcuts import render from matplotlib.figure import Figure from ocflib.lab import stats -from ocflib.printing.printers import PRINTERS from ocflib.printing.quota import get_connection from ocflib.printing.quota import SEMESTERLY_QUOTA from ocfweb.caching import periodic from ocfweb.component.graph import plot_to_image_bytes +from ocfweb.printing import get_printers -ALL_PRINTERS = ('papercut', 'pagefault', 'logjam', 'logjam-old', 'deforestation') -ACTIVE_PRINTERS = ('papercut', 'pagefault', 'logjam') +ALL_PRINTERS = ('papercut', 'pagefault', 'logjam', 'logjam-old', 'deforestation', 'OCF-BW', 'OCF-Color') +ACTIVE_PRINTERS = ('OCF-BW', 'OCF-Color') def stats_printing(request: HttpRequest) -> HttpResponse: @@ -29,7 +29,7 @@ def stats_printing(request: HttpRequest) -> HttpResponse: 'stats/printing.html', { 'title': 'Printing Statistics', - 'current_printers': PRINTERS, + 'current_printers': get_printers().keys(), 'toner_changes': _toner_changes(), 'last_month': [ date.today() - timedelta(days=i) @@ -75,7 +75,7 @@ def _toner_changes() -> List[Any]: printer, _toner_used_by_printer(printer), ) - for printer in ACTIVE_PRINTERS + for printer in get_printers().keys() ] @@ -197,13 +197,14 @@ def _pages_printed_for_printer(printer: str, resolution: int = 100) -> List[Any] @periodic(3600) def _pages_printed_data() -> List[Any]: + printers = set(ALL_PRINTERS) | set(get_printers().keys()) return [ { 'name': printer, 'animation': False, 'data': _pages_printed_for_printer(printer), } - for printer in ALL_PRINTERS + for printer in sorted(printers) ] diff --git a/ocfweb/stats/summary.py b/ocfweb/stats/summary.py index 02142960b..6a8f7f823 100644 --- a/ocfweb/stats/summary.py +++ b/ocfweb/stats/summary.py @@ -20,9 +20,9 @@ from ocflib.lab.stats import UtilizationProfile from ocflib.printing.printers import get_maintkit from ocflib.printing.printers import get_toner -from ocflib.printing.printers import PRINTERS from ocfweb.caching import periodic +from ocfweb.printing import get_printers from ocfweb.stats.daily_graph import get_open_close _logger = logging.getLogger(__name__) @@ -86,9 +86,10 @@ def inner(*args: Any, **kwargs: Any) -> Any: return None return inner + printer_names = get_printers().keys() return sorted( (printer, silence(get_toner)(printer), silence(get_maintkit)(printer)) - for printer in PRINTERS + for printer in printer_names ) diff --git a/ocfweb/templates/base.html b/ocfweb/templates/base.html index a25de9ba7..d8664d091 100644 --- a/ocfweb/templates/base.html +++ b/ocfweb/templates/base.html @@ -59,6 +59,8 @@