mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-23 23:43:47 +00:00
Printer App a beginning.
This commit is contained in:
parent
8184f13ff7
commit
a39c39cc99
11 changed files with 412 additions and 5 deletions
37
printer/forms.py
Normal file
37
printer/forms.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""printer.forms
|
||||||
|
Form to add, edit, cancel printer jobs.
|
||||||
|
Author : Maxime Bombar <bombar@crans.org>.
|
||||||
|
Date : 29/06/2018
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.forms import (
|
||||||
|
Form,
|
||||||
|
ModelForm,
|
||||||
|
)
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from re2o.mixins import FormRevMixin
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
JobWithOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JobForm(FormRevMixin, ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
|
||||||
|
super(TrueJobForm, self).__init__(*args, prefix=prefix, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = JobWithOptions
|
||||||
|
fields = [
|
||||||
|
'file',
|
||||||
|
'color',
|
||||||
|
'disposition',
|
||||||
|
'count',
|
||||||
|
]
|
||||||
|
|
|
@ -1,3 +1,116 @@
|
||||||
from django.db import models
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
|
|
||||||
# Create your models here.
|
"""printer.models
|
||||||
|
Models of the printer application
|
||||||
|
Author : Maxime Bombar <bombar@crans.org>.
|
||||||
|
Date : 29/06/2018
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.forms import ValidationError
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.template.defaultfilters import filesizeformat
|
||||||
|
|
||||||
|
from re2o.mixins import RevMixin
|
||||||
|
|
||||||
|
import users.models
|
||||||
|
|
||||||
|
from .validators import (
|
||||||
|
FileValidator,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .settings import (
|
||||||
|
MAX_PRINTFILE_SIZE,
|
||||||
|
ALLOWED_TYPES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
- ```user_printing_path``` is a function that returns the path of the uploaded file, used with the FileField.
|
||||||
|
- ```Job``` is the main model of a printer job. His parent is the ```user``` model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def user_printing_path(instance, filename):
|
||||||
|
# File will be uploaded to MEDIA_ROOT/printings/user_<id>/<filename>
|
||||||
|
return 'printings/user_{0}/{1}'.format(instance.user.id, filename)
|
||||||
|
|
||||||
|
|
||||||
|
class JobWithOptions(RevMixin, models.Model):
|
||||||
|
"""
|
||||||
|
This is the main model of printer application :
|
||||||
|
|
||||||
|
- ```user``` is a ForeignKey to the User Application
|
||||||
|
- ```file``` is the file to print
|
||||||
|
- ```starttime``` is the time when the job was launched
|
||||||
|
- ```endtime``` is the time when the job was stopped.
|
||||||
|
A job is stopped when it is either finished or cancelled.
|
||||||
|
- ```status``` can be running, finished or cancelled.
|
||||||
|
- ```club``` is blank in general. If the job was launched as a club then
|
||||||
|
it is the id of the club.
|
||||||
|
- ```price``` is the total price of this printing.
|
||||||
|
|
||||||
|
Printing Options :
|
||||||
|
|
||||||
|
- ```format``` is the paper format. Example: A4.
|
||||||
|
- ```color``` is the colorization option. Either Color or Greyscale.
|
||||||
|
- ```disposition``` is the paper disposition.
|
||||||
|
- ```count``` is the number of copies to be printed.
|
||||||
|
- ```stapling``` is the stapling options.
|
||||||
|
- ```perforations``` is the perforation options.
|
||||||
|
|
||||||
|
|
||||||
|
Parent class : User
|
||||||
|
"""
|
||||||
|
STATUS_AVAILABLE = (
|
||||||
|
('Printable', 'Printable'),
|
||||||
|
('Running', 'Running'),
|
||||||
|
('Cancelled', 'Cancelled'),
|
||||||
|
('Finished', 'Finished')
|
||||||
|
)
|
||||||
|
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
|
||||||
|
file = models.FileField(upload_to=user_printing_path, validators=[FileValidator(allowed_types=ALLOWED_TYPES, max_size=MAX_PRINTFILE_SIZE)])
|
||||||
|
starttime = models.DateTimeField(auto_now_add=True)
|
||||||
|
endtime = models.DateTimeField(null=True)
|
||||||
|
status = models.CharField(max_length=255, choices=STATUS_AVAILABLE)
|
||||||
|
printAs = models.ForeignKey('users.User', on_delete=models.PROTECT, related_name='print_as_user', null=True)
|
||||||
|
price = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
FORMAT_AVAILABLE = (
|
||||||
|
('A4', 'A4'),
|
||||||
|
('A3', 'A4'),
|
||||||
|
)
|
||||||
|
COLOR_CHOICES = (
|
||||||
|
('Greyscale', 'Greyscale'),
|
||||||
|
('Color', 'Color')
|
||||||
|
)
|
||||||
|
DISPOSITIONS_AVAILABLE = (
|
||||||
|
('TwoSided', 'Two sided'),
|
||||||
|
('OneSided', 'One sided'),
|
||||||
|
('Booklet', 'Booklet')
|
||||||
|
)
|
||||||
|
STAPLING_OPTIONS = (
|
||||||
|
('None', 'None'),
|
||||||
|
('TopLeft', 'One top left'),
|
||||||
|
('TopRight', 'One top right'),
|
||||||
|
('LeftSided', 'Two left sided'),
|
||||||
|
('RightSided', 'Two right sided')
|
||||||
|
)
|
||||||
|
PERFORATION_OPTIONS = (
|
||||||
|
('None', 'None'),
|
||||||
|
('TwoLeftSidedHoles', 'Two left sided holes'),
|
||||||
|
('TwoRightSidedHoles', 'Two right sided holes'),
|
||||||
|
('TwoTopHoles', 'Two top holes'),
|
||||||
|
('TwoBottomHoles', 'Two bottom holes'),
|
||||||
|
('FourLeftSidedHoles', 'Four left sided holes'),
|
||||||
|
('FourRightSidedHoles', 'Four right sided holes')
|
||||||
|
)
|
||||||
|
|
||||||
|
format = models.CharField(max_length=255, choices=FORMAT_AVAILABLE, default='A4')
|
||||||
|
color = models.CharField(max_length=255, choices=COLOR_CHOICES, default='Greyscale')
|
||||||
|
disposition = models.CharField(max_length=255, choices=DISPOSITIONS_AVAILABLE, default='TwoSided')
|
||||||
|
count = models.PositiveIntegerField(default=1)
|
||||||
|
stapling = models.CharField(max_length=255, choices=STAPLING_OPTIONS, default='None')
|
||||||
|
perforation = models.CharField(max_length=255, choices=PERFORATION_OPTIONS, default='None')
|
||||||
|
|
12
printer/templates/printer/echec.html
Normal file
12
printer/templates/printer/echec.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load massive_bootstrap_form %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %}Printing interface{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>{% trans "Failure" %}</h3>
|
||||||
|
{% endblock %}
|
87
printer/templates/printer/newjob.html
Normal file
87
printer/templates/printer/newjob.html
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load massive_bootstrap_form %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %}Printing interface{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<h3>{% trans "Printing Menu" %}</h3>
|
||||||
|
{{ jobform.management_form }}
|
||||||
|
{% bootstrap_formset_errors jobform %}
|
||||||
|
<div id="form_set" class="form-group">
|
||||||
|
{% for job in jobform.forms %}
|
||||||
|
<div class='file_to_print form-inline'>
|
||||||
|
{% bootstrap_form job label_class='sr-only' %}
|
||||||
|
<button class="btn btn-danger btn-sm" id="id_form-0-job-remove" type="button">
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<input class="btn btn-primary btn-sm" role="button" value="{% trans "Add a file"%}" id="add_one">
|
||||||
|
{% bootstrap_button action_name button_type="submit" icon="star" %}
|
||||||
|
</form>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
var template = `{% bootstrap_form jobform.empty_form label_class='sr-only' %}
|
||||||
|
<button class="btn btn-danger btn-sm"
|
||||||
|
id="id_form-__prefix__-job-remove" type="button">
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
</button>`
|
||||||
|
|
||||||
|
function add_job() {
|
||||||
|
var new_index =
|
||||||
|
document.getElementsByClassName('file_to_print').length;
|
||||||
|
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
||||||
|
var new_job = document.createElement('div');
|
||||||
|
new_job.className = 'file_to_print form-inline';
|
||||||
|
new_job.innerHTML = template.replace(/__prefix__/g, new_index);
|
||||||
|
document.getElementById('form_set').appendChild(new_job);
|
||||||
|
add_listener_for_id(new_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function del_job(event){
|
||||||
|
var job = event.target.parentNode;
|
||||||
|
job.parentNode.removeChild(job);
|
||||||
|
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function add_listener_for_id(i){
|
||||||
|
document.getElementById('id_form-' + i.toString() + '-job-remove')
|
||||||
|
.addEventListener("click", function(event){
|
||||||
|
var job = event.target.parentNode;
|
||||||
|
job.parentNode.removeChild(job);
|
||||||
|
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Add events manager when DOM is fully loaded
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
function() {
|
||||||
|
document.getElementById("add_one")
|
||||||
|
.addEventListener("click", add_job, true);
|
||||||
|
document.getElementById('id_form-0-job-remove')
|
||||||
|
.addEventListener("click", function(event){
|
||||||
|
var job = event.target.parentNode;
|
||||||
|
job.parentNode.removeChild(job);
|
||||||
|
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
12
printer/templates/printer/success.html
Normal file
12
printer/templates/printer/success.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load massive_bootstrap_form %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %}Printing interface{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>{% trans "Success" %}</h3>
|
||||||
|
{% endblock %}
|
|
@ -1,3 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
"""printer.urls
|
||||||
|
The defined URLs for the printer app
|
||||||
|
Author : Maxime Bombar <bombar@crans.org>.
|
||||||
|
Date : 29/06/2018
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
urlpatterns = []
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
import re2o
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^new_job/$', views.new_job, name="new-job"),
|
||||||
|
url(r'^success/$', views.success, name="success"),
|
||||||
|
]
|
||||||
|
|
72
printer/validators.py
Normal file
72
printer/validators.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
"""printer.validators
|
||||||
|
Custom validators useful for printer application.
|
||||||
|
Author : Maxime Bombar <bombar@crans.org>.
|
||||||
|
Date : 29/06/2018
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.template.defaultfilters import filesizeformat
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class FileValidator(object):
|
||||||
|
"""
|
||||||
|
Custom validator for files. It checks the size and mimetype.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* ```allowed_types``` is an iterable of allowed mimetypes. Example: ['application/pdf'] for a pdf file.
|
||||||
|
* ```max_size``` is the maximum size allowed in bytes. Example: 25*1024*1024 for 25 MB.
|
||||||
|
|
||||||
|
Usage example:
|
||||||
|
|
||||||
|
class UploadModel(models.Model):
|
||||||
|
file = fileField(..., validators=FileValidator(allowed_types = ['application/pdf'], max_size=25*1024*1024))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize the custom validator.
|
||||||
|
By default, all types and size are allowed.
|
||||||
|
"""
|
||||||
|
self.allowed_types = kwargs.pop('allowed_types', None)
|
||||||
|
self.max_size = kwargs.pop('max_size', None)
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""
|
||||||
|
Check the type and size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
type_message = _("MIME type '%(type)s' is not valid. Please, use one of these types: %(allowed_types)s.")
|
||||||
|
type_code = 'invalidType'
|
||||||
|
|
||||||
|
oversized_message = _('The current file size is %(size)s. The maximum file size is %(max_size)s.')
|
||||||
|
oversized_code = 'oversized'
|
||||||
|
|
||||||
|
|
||||||
|
mimetype = mimetypes.guess_type(value.name)[0]
|
||||||
|
if self.allowed_types and not (mimetype in self.allowed_types):
|
||||||
|
type_params = {
|
||||||
|
'type': mimetype,
|
||||||
|
'allowed_types': ', '.join(self.allowed_types),
|
||||||
|
}
|
||||||
|
|
||||||
|
raise ValidationError(type_message, code=type_code, params=type_params)
|
||||||
|
|
||||||
|
filesize = len(value)
|
||||||
|
if self.max_size and filesize > self.max_size:
|
||||||
|
oversized_params = {
|
||||||
|
'size': '{}'.format(filesizeformat(filesize)),
|
||||||
|
'max_size': '{}'.format(filesizeformat(self.max_size)),
|
||||||
|
}
|
||||||
|
|
||||||
|
raise ValidationError(oversized_message, code=oversized_code, params=oversized_params)
|
|
@ -1,3 +1,55 @@
|
||||||
from django.shortcuts import render
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
|
|
||||||
# Create your views here.
|
"""printer.views
|
||||||
|
The views for the printer app
|
||||||
|
Author : Maxime Bombar <bombar@crans.org>.
|
||||||
|
Date : 29/06/2018
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.forms import modelformset_factory, formset_factory
|
||||||
|
|
||||||
|
from re2o.views import form
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
from . import settings
|
||||||
|
|
||||||
|
from .forms import (
|
||||||
|
JobForm,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def new_job(request):
|
||||||
|
"""
|
||||||
|
View to create a new printing job
|
||||||
|
"""
|
||||||
|
job_formset = formset_factory(JobForm)(
|
||||||
|
request.POST or None, request.FILES,
|
||||||
|
)
|
||||||
|
if job_formset.is_valid():
|
||||||
|
for job in job_formset:
|
||||||
|
job = job.save(commit=False)
|
||||||
|
job.user=request.user
|
||||||
|
job.status='Printable'
|
||||||
|
job.save()
|
||||||
|
return redirect(reverse(
|
||||||
|
'printer:success',
|
||||||
|
))
|
||||||
|
return form(
|
||||||
|
{
|
||||||
|
'jobform': job_formset,
|
||||||
|
'action_name': "Print",
|
||||||
|
},
|
||||||
|
'printer/newjob.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
def success(request):
|
||||||
|
return form(
|
||||||
|
{},
|
||||||
|
'printer/success.html',
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
|
@ -75,6 +75,7 @@ LOCAL_APPS = (
|
||||||
're2o',
|
're2o',
|
||||||
'preferences',
|
'preferences',
|
||||||
'logs',
|
'logs',
|
||||||
|
'printer',
|
||||||
)
|
)
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
DJANGO_CONTRIB_APPS +
|
DJANGO_CONTRIB_APPS +
|
||||||
|
|
|
@ -72,6 +72,7 @@ urlpatterns = [
|
||||||
r'^preferences/',
|
r'^preferences/',
|
||||||
include('preferences.urls', namespace='preferences')
|
include('preferences.urls', namespace='preferences')
|
||||||
),
|
),
|
||||||
|
url(r'^printer/', include('printer.urls', namespace='printer')),
|
||||||
]
|
]
|
||||||
# Add debug_toolbar URLs if activated
|
# Add debug_toolbar URLs if activated
|
||||||
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||||
|
|
|
@ -112,6 +112,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="glyphicon glyphicon-print"></i> Printer<span class="caret"></span></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{% url "printer:new-job" %}"><i class="fa fa-print"></i> {% trans "Print" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
{% can_view_app logs %}
|
{% can_view_app logs %}
|
||||||
<li><a href="{% url "logs:index" %}"><i class="fa fa-chart-area"></i> {% trans "Statistics" %}</a></li>
|
<li><a href="{% url "logs:index" %}"><i class="fa fa-chart-area"></i> {% trans "Statistics" %}</a></li>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
|
|
Loading…
Reference in a new issue