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. +
+ + ++ Quota is deducted automatically when you print. + For more info, see the printing docs. +
+