Skip to content
Open
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ RUN apt-get update \
cracklib-runtime \
libcrack2 \
libcrack2-dev \
libcups2-dev \
libffi-dev \
libfreetype6-dev \
libpng-dev \
Expand Down
4 changes: 4 additions & 0 deletions conf/ocfweb.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ db = ocfmail
user = ocfstats
password = password
db = ocfstats

[printing]
# Joe's VM
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using joe's staff vm is not acceptable for deployment, also there's probably better ways to do this

otp_url = http://joe:15011
78 changes: 10 additions & 68 deletions devenv.lock
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did the devenv get updated as a side effect to this pr

Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -110,11 +56,7 @@
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"git-hooks"
]
"nixpkgs": "nixpkgs"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
uv.enable = true;
};
packages = with pkgs; [
cups
gnumake
libffi
pkg-config
Expand Down
118 changes: 118 additions & 0 deletions ocfweb/account/print.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
43 changes: 43 additions & 0 deletions ocfweb/account/templates/account/print.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% load bootstrap %}

{% block content %}
<div class="row">
<div class="col-sm-8 ocf-content-block">
<p>
Welcome to web printing! You can upload a file here to print it to one of the lab printers.
</p>

<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div style="max-width: 400px;">
{{ form|bootstrap }}
</div>
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-print" aria-hidden="true"></span>
Print
</button>
</form>
</div>

<div class="col-sm-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Remaining Quota</h3>
</div>
<div class="panel-body">
<dl>
<dt>Daily</dt>
<dd>{{ quota.daily }} pages</dd>
<dt>Semesterly</dt>
<dd>{{ quota.semesterly }} pages</dd>
</dl>
<p class="text-muted small">
Quota is deducted automatically when you print.
For more info, see the <a href="{% url 'doc' 'services/lab/printing' %}">printing docs</a>.
</p>
</div>
</div>
</div>
</div>
{% endblock %}
2 changes: 2 additions & 0 deletions ocfweb/account/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'),
Expand Down
11 changes: 11 additions & 0 deletions ocfweb/printing.py
Original file line number Diff line number Diff line change
@@ -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 {}
2 changes: 2 additions & 0 deletions ocfweb/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'] = {
Expand Down
13 changes: 7 additions & 6 deletions ocfweb/stats/printing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't use the aggregate names here, these variables are referenced elsewhere in ocfweb

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my stats branch for the proper way to do this, although it's far from finished https://github.com/ocf/ocfweb/tree/printer-stats-update

ACTIVE_PRINTERS = ('OCF-BW', 'OCF-Color')


def stats_printing(request: HttpRequest) -> HttpResponse:
Expand All @@ -29,7 +29,7 @@ def stats_printing(request: HttpRequest) -> HttpResponse:
'stats/printing.html',
{
'title': 'Printing Statistics',
'current_printers': PRINTERS,
'current_printers': get_printers().keys(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you update the variables above and then not use them

'toner_changes': _toner_changes(),
'last_month': [
date.today() - timedelta(days=i)
Expand Down Expand Up @@ -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()
]


Expand Down Expand Up @@ -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)
]


Expand Down
Loading