def on_model_change(self, form, model, is_created): if is_created: if bool(form.file.data) == form.no_file.data: raise ValidationError( 'Genau ein Feld von "Datei" oder "Keine Datei" muss angegeben werden.' ) if model.id is None: sqla.session.flush() # acquire ID if form.file.data: self._delete_document(model) # delete old file save_file(model, form.file.data) if form.form_validated.data != model.validated: # validation state changed if form.form_validated.data: model.validation_time = datetime.datetime.now() else: model.validation_time = None if model.validated and not model.has_barcode: if model.document_type != 'written': barcode.bake_barcode(model) config.document_validated(document_path(model.id)) model.has_barcode = True # this is the correct behaviour, although the has_barcode name does not indicate it super().on_model_change(form, model, is_created)
def on_model_change(self, form, model, is_created): if is_created: if bool(form.file.data) == form.no_file.data: raise ValidationError('Genau ein Feld von "Datei" oder "Keine Datei" muss angegeben werden.') model.validated = True if model.id is None: sqla.session.flush() # acquire ID got_new_file = False if form.file.data: self._delete_document(model) # delete old file save_file(model, form.file.data) if model.validated: got_new_file = True if model.validation_time is None and model.validated: # document has just been validated for the first time model.validation_time = datetime.datetime.now() got_new_file = model.has_file if got_new_file: if model.document_type != 'written': barcode.bake_barcode(model) config.document_validated(document_path(model.id)) super().on_model_change(form, model, is_created)
def on_model_change(self, form, model, is_created): if is_created: if bool(form.file.data) == form.no_file.data: raise ValidationError('Genau ein Feld von "Datei" oder "Keine Datei" muss angegeben werden.') if model.id is None: sqla.session.flush() # acquire ID if form.file.data: self._delete_document(model) # delete old file save_file(model, form.file.data) if form.form_validated.data != model.validated: # validation state changed if form.form_validated.data: model.validation_time = datetime.datetime.now() else: model.validation_time = None if model.validated and not model.has_barcode: if model.document_type != 'written': barcode.bake_barcode(model) config.document_validated(document_path(model.id)) model.has_barcode = True # this is the correct behaviour, although the has_barcode name does not indicate it super().on_model_change(form, model, is_created)
def _delete_document(self, model): if model.has_file: source = document_path(model.id) if os.path.exists(source): dest = os.path.join(config.DOCUMENT_DIRECTORY, 'trash', str(model.id)) while os.path.exists(dest + '.pdf'): dest += 'lol' os.renames(source, dest + '.pdf')
def view_document(instance_id): if get_user() or is_kiosk(): doc = Document.query.get(instance_id) if doc is None or not doc.has_file: raise ClientError('document not found', status=404) return send_file(document_path(doc.id)) else: return unauthorized()
def view_document(instance_id): doc = Document.query.get(instance_id) if get_user() or is_kiosk() or doc.publicly_available: if doc is None or not doc.has_file: raise ClientError('document not found', status=404) return send_file(document_path(doc.id), as_attachment=(request.args.get('download') is not None)) else: return unauthorized()
def submit_documents(validated): """Student document submission endpoint POST data must be multipart, with the `json` part conforming to DocumentLoadSchema and the `file` part being a pdf file. Uploaded files are stored in subdirectories below config.DOCUMENT_DIRECTORY. This method may raise AssertionErrors when the user sends invalid data. """ # we can't use @deserialize because this endpoint uses multipart form data data = json.loads(request.form['json']) if get_user(): (data, errors) = FullDocumentLoadSchema().load(data) if (data.get('document_type') == 'written' and data.get('solution') not in ['official', 'inofficial', 'none'] or data.get('document_type') != 'written' and data.get('solution') is not None): errors['solution'] = 'Invalid value.' else: (data, errors) = DocumentLoadSchema().load(data) if errors: raise ClientError(*errors) assert 'file' in request.files file = request.files['file'] if not _allowed_file(file.filename): raise ClientError('file extension not allowed', status=406) lectures = _match_lectures(data['lectures'], validated) examinants = _match_examinants(data['examinants'], validated) date = data['date'] if not get_user(): assert date <= datetime.date.today() new_doc = Document( department=data['department'], lectures=lectures, examinants=examinants, date=date, number_of_pages=0, # will be filled in later or upon validation document_type=data['document_type'], validated=validated, validation_time=datetime.datetime.now() if validated else None, comment=data.get('comment'), solution=data.get('solution'), submitted_by=data.get('student_name')) sqla.session.add(new_doc) # we have the db side of things taken care of, now save the file # and tell the db where to find the file sqla.session.flush() # necessary for id field to be populated save_file(new_doc, file) if validated: # we don't trust other people's PDFs... new_doc.number_of_pages = number_of_pages(new_doc) sqla.session.commit() app.logger.info("New document submitted (id: {})".format(new_doc.id)) if validated: config.document_validated(document_path(new_doc.id)) return {}
def bake_barcode(document): """Put a generated barcode onto the PDF The pipeline works as follows: barcode.ps is piped into ps2pdf, followed by the call to the barcode-generating function. The resulting one-page PDF containing only the barcode is read into memory. This is then piped into pdftk, which grafts the barcode onto the first page of the document (discarding all other pages). We dump all but the first page of the document and concatenate our modified first page to it (via pdfjam). If this sounds somewhat roundabout, that's because it is. """ # if the document has a legacy_id, the PDF already has a barcode. if document.legacy_id or not document.has_file: return doc_path = document_path(document.id) # The barcodes we use have 13 digits. The last one is a checksum digit. barcode.ps takes care of this. barcode = GS1_NAMESPACE + str(document.id).zfill(12 - len(GS1_NAMESPACE)) with open(BARCODE_PS_FILE, 'rb') as barcode_file: ps2pdf = subprocess.Popen(['ps2pdf', '-', '-'], stdin=PIPE, stdout=PIPE) (barcode_pdf, _) = ps2pdf.communicate(barcode_file.read() + BARCODE_TEMPLATE.format( xpos=XPOS, ypos=YPOS, barcode=barcode).encode('utf-8')) assert ps2pdf.returncode == 0 # pdftk outputs a pdf with only one page, discarding the rest of the PDF pdftk = subprocess.Popen( ['pdftk', '-', 'background', doc_path, 'output', '-'], stdin=PIPE, stdout=PIPE) (pdf_with_barcode, _) = pdftk.communicate(barcode_pdf) if document.number_of_pages > 1: rest_path = _tmp_path(document, '-rest.pdf') subprocess.check_call( ['pdfjam', '--nup', '1x1', doc_path, '2-', '--outfile', rest_path], stderr=DEVNULL) pdfjam = subprocess.Popen([ 'pdfjam', '--fitpaper', 'true', '/dev/stdin', rest_path, '--outfile', doc_path ], stdin=PIPE, stderr=DEVNULL) pdfjam.communicate(pdf_with_barcode) os.unlink(rest_path) else: with open(doc_path, 'wb') as document_file: document_file.write(pdf_with_barcode)
def print_all(self): folders = list(self._get_folders_with_counts()) for folder, _ in folders: documents = self._query_unprinted(folder).all() config.print_documents( doc_paths=[document_path(doc.id) for doc in documents], cover_text=None, printer=config.FS_CONFIG['OFFICES'][self._get_location()]['printers'][0], usercode=config.PRINTER_USERCODES['internal'], job_title='Odie-Druck: Ordnerdruck' ) folder.printed_docs += documents sqla.session.commit() return self.render('print_for_folder.html', printed_folders=folders, examinants_without_folders=Examinant.query.filter(~Examinant.folders.any()).sort_by(Examinant.name).all())
def delete_document(id): doc = Document.query.get(id) if doc is None: raise ClientError('document not found') if doc.validated or (not doc.early_document_eligible and not doc.deposit_return_eligible): raise ClientError('document not eligible for deletion') sqla.session.delete(doc) if doc.has_file: source = document_path(doc.id) if os.path.exists(source): dest = os.path.join(config.DOCUMENT_DIRECTORY, 'trash', str(doc.id)) while os.path.exists(dest + '.pdf'): dest += 'lol' os.renames(source, dest + '.pdf') sqla.session.commit() return {}
def print_all(self): folders = list(self._get_folders_with_counts()) for folder, _ in folders: documents = self._query_unprinted(folder).all() config.print_documents( doc_paths=[document_path(doc.id) for doc in documents], cover_text=None, printer=config.FS_CONFIG['OFFICES'][ self._get_location()]['printers'][0], user=get_user().username, usercode=config.PRINTER_USERCODES['internal'], job_title='Odie-Druck: Ordnerdruck') folder.printed_docs += documents sqla.session.commit() return self.render('print_for_folder.html', printed_folders=folders, examinants_without_folders=Examinant.query.filter( ~Examinant.folders.any()).sort_by( Examinant.name).all())
def print_documents(): # GET params could be too limited. therefore, cookies. try: data = PrintJobLoadSchema().loads(urllib.parse.unquote(request.cookies['print_data'])) except marshmallow.exception.ValidationError as e: raise ClientError(str(e), status=500) document_ids = data['document_ids'] app.logger.info("Printing document ids {} ({} in total) on {} for {}".format(document_ids, len(document_ids), data['printer'], data['cover_text'])) document_objs = Document.query.filter(Document.id.in_(document_ids)).all() if any(not doc.has_file for doc in document_objs): raise ClientError('Tried to print at least one document without file', status=400) # sort the docs into the same order they came in the request docs_by_id = {doc.id: doc for doc in document_objs} documents = [docs_by_id[id] for id in document_ids] assert data['deposit_count'] >= 0 print_price = sum(doc.price for doc in documents) # round up to next 10 cents print_price = 10 * (print_price//10 + (1 if print_price % 10 else 0)) assert print_price + data['deposit_count'] * config.FS_CONFIG['DEPOSIT_PRICE'] == data['price'] # pull current user out of app context user = get_user() yield None # exit application context, start event stream num_pages = sum(doc.number_of_pages for doc in documents) try: if documents: db.accounting.log_exam_sale(num_pages, print_price, user, data['cash_box']) for _ in range(data['deposit_count']): lectures = Lecture.query.filter(Lecture.documents.any(Document.id.in_(document_ids))).all() dep = Deposit( price=config.FS_CONFIG['DEPOSIT_PRICE'], name=data['cover_text'], by_user=user.full_name, lectures=lectures) sqla.session.add(dep) db.accounting.log_deposit(dep, user, data['cash_box']) if documents: try: name = data['cover_text'].split(' ')[0] for _ in config.print_documents( doc_paths=[document_path(doc.id) for doc in documents], cover_text=data['cover_text'], printer=data['printer'], user=user.username, usercode=config.PRINTER_USERCODES[data['cash_box']], job_title="Odie-Druck für {} [{} Seiten]".format(name, num_pages)): pass except Exception as e: sqla.session.rollback() raise NonConfidentialException('printing failed. Exception: ' + str(e)) from e sqla.session.commit() yield('accounting succeeded', '') except NonConfidentialException as e: raise NonConfidentialException(str(e)) from e except Exception as e: # in case of network troubles, we've just printed a set of documents but screwed up accounting. raise NonConfidentialException('accounting failed. Exception: ' + str(e)) from e yield ('complete', '')
def submit_documents(validated): """Student document submission endpoint POST data must be multipart, with the `json` part conforming to DocumentLoadSchema and the `file` part being a pdf file. Uploaded files are stored in subdirectories below config.DOCUMENT_DIRECTORY. This method may raise AssertionErrors when the user sends invalid data. """ # we can't use @deserialize because this endpoint uses multipart form data data = json.loads(request.form['json']) if get_user(): try: data = FullDocumentLoadSchema().load(data) except marshmallow.exceptions.ValidationError as e: raise ClientError(str(e), status=500) if (data.get('document_type') == 'written' and data.get('solution') not in ['official', 'inofficial', 'none'] or data.get('document_type') != 'written' and data.get('solution') is not None): raise ClientError('Invalid value.', status=400) else: try: data = DocumentLoadSchema().load(data) except marshmallow.exceptions.ValidationError as e: raise ClientError(str(e), status=400) assert 'file' in request.files file = request.files['file'] if not _allowed_file(file.filename): raise ClientError('file extension not allowed', status=406) lectures = _match_lectures(data['lectures'], validated) examinants = _match_examinants(data['examinants'], validated) date = data['date'] if not get_user(): assert date <= datetime.date.today() doc_type = data.get('document_type') student_name = data.get('student_name') if student_name is None or student_name.isspace(): student_name = None deposit_return_eligible = student_name is not None early_document_eligible = student_name is not None and doc_type == 'oral' and any( lecture.early_document_eligible for lecture in lectures) new_doc = Document( department=data['department'], lectures=lectures, examinants=examinants, date=date, number_of_pages=0, # will be filled in later or upon validation document_type=doc_type, validation_time=datetime.datetime.now() if validated else None, comment=data.get('comment'), solution=data.get('solution'), submitted_by=student_name, early_document_eligible=early_document_eligible, deposit_return_eligible=deposit_return_eligible) sqla.session.add(new_doc) # we have the db side of things taken care of, now save the file # and tell the db where to find the file sqla.session.flush() # necessary for id field to be populated save_file(new_doc, file) if validated: # we don't trust other people's PDFs... new_doc.number_of_pages = number_of_pages(new_doc) sqla.session.commit() app.logger.info("New document submitted (id: {})".format(new_doc.id)) if validated: config.document_validated(document_path(new_doc.id)) return {'early_document_eligible': early_document_eligible}