mirror of https://gitlab.federez.net/re2o/re2o
11 changed files with 411 additions and 4 deletions
@ -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 @@ |
|||
# -*- mode: python; coding: utf-8 -*- |
|||
|
|||
"""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') |
|||
) |
|||
|
|||
# Create your models here. |
|||
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') |
|||
|
|||
@ -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 %} |
|||
@ -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 %} |
|||
|
|||
@ -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 -*- |
|||
"""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"), |
|||
] |
|||
|
|||
@ -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 |
|||
) |
|||
|
|||
Loading…
Reference in new issue