pdnew/upload.py

306 lines
8.8 KiB
Python
Raw Permalink Normal View History

# builtins
import os
import mimetypes
# third-party
import markupsafe
import werkzeug
import flask
import peewee
# internals
from application import app
import form
import database
import admin
import markdown
@app.abstract
class Upload(admin.Named):
subdirectory = None
published = peewee.BooleanField(null=False, default=False, verbose_name='Published')
filename = peewee.CharField(null=False, unique=True)
description = markdown.MarkdownTextField(null=True)
allowed_extensions = '*'
@property
def path(self):
return f"upload/{self.subdirectory}/{self.filename}"
@property
def media_type(self):
# essentially the same logic werkzeug.utils.send_file uses
mimetype, _ = mimetypes.guess_type(self.filename)
if mimetype:
return mimetype
return 'application/octet-stream'
@property
def filesize(self):
try:
return os.path.getsize(self.path)
except OSError:
return float('nan') # not a number
def render(self, mode='full', format='html'):
if not (self.published or flask.g.user):
return markupsafe.Markup('<div class="embed-error">Unpublished embedded content</div>')
return super().render(mode=mode, format=format)
def form(self, mode, **kwargs):
f = super().form(mode, **kwargs)
if self.is_in_db:
teaser = self.render('inline')
if f.intro:
f.intro = teaser + f.intro
else:
f.intro = teaser
return f
def form_field(self, field_name):
if field_name == 'filename':
db_field = getattr(self.__class__, field_name)
field_label = 'File'
field_help = db_field.help_text
field_value = getattr(self, field_name)
field_required = not self.id
return form.File(label=field_label, value=field_value, help=field_help, required=field_required)
return super().form_field(field_name)
def form_field_handle(self, field, mode):
if field.name == 'filename':
if field.valid and field.value:
self.write_file(field.value)
flask.flash("Stored new file.", 'info')
def write_file(self, file: werkzeug.datastructures.file_storage.FileStorage):
extension = file.filename.split('.')[-1]
filename = f'{self.name}.{extension}'
filename_clean = werkzeug.utils.secure_filename(filename)
if self.filename:
try:
os.unlink(self.path)
except FileNotFoundError as e:
app.logger.warning("Didn't find associated file '{self.filename}' for {self.__class__.__name__} '{self.name}', storing new file.")
self.filename = filename_clean
file.save(self.path)
def url(self, mode='raw'):
if mode in ('raw', 'download'):
url = f'/upload/{self.subdirectory}/{self.name}/' # FIXME: make this less hacky, if possible
if mode == 'download':
url += '?download'
return url
return super().url(mode=mode)
def delete_instance(self, **kwargs):
try:
os.unlink(self.path)
except FileNotFoundError as e:
# all other exception types not caught so db record won't be deleted
app.logger.warning("Didn't find associated file '{self.filename}' for {self.__class__.__name__} '{self.name}', proceeding with record deletion.")
flask.flash(f"Didn't find associated file '{self.filename}', deleting record anyhow.", 'warning')
super().delete_instance(**kwargs)
class File(Upload):
# NOTE: collides with form.File in ClassAware attributes
# this is fine tho, because it's defined later (i.e. overwrites)
# references to form.File which you never want to look up through
# ClassAware anyhow. And if you do, you can still go through a
# non-common Ancestor, i.e. form.Field
subdirectory = 'file'
class Image(Upload):
subdirectory = 'image'
class Audio(Upload):
subdirectory = 'audio'
class Video(Upload):
subdirectory = 'video'
class GalleryForm(admin.AutoForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.mode == 'edit':
self['items'] = form.Fieldset()
for item in self.administerable.items_ordered:
fieldset_item = form.ProxyFieldset(item.form('edit'), name=f'item-{item.id}')
fieldset_item['gallery_id'] = form.ValueField(value=self.administerable.id)
del(fieldset_item['order_pos'])
fieldset_delete = form.ProxyFieldset(item.form('delete'), name=f'item-{item.id}-delete')
fieldset_delete.intro = None
fieldset_delete.buttons['delete'].label = ''
fieldset_order = form.Fieldset(name='order')
fieldset_order.buttons['up'] = form.Button(name='up', label='')
fieldset_order.buttons['down'] = form.Button(name='down', label='')
# HERE
fieldset = form.Fieldset(name=f'{fieldset_item.name}-wrapper', extra_classes=['galleryitem-wrapper'])
fieldset['item'] = fieldset_item
fieldset['delete'] = fieldset_delete
fieldset['order'] = fieldset_order
self['items'][fieldset.name] = fieldset
2024-04-19 12:04:53 +00:00
fieldset = form.ProxyFieldset(GalleryItem().form('create'), name='item-new')
fieldset['gallery_id'] = form.ValueField(value=self.administerable.id)
del(fieldset['order_pos'])
self['items']['new'] = fieldset
def handle(self, skip_bind=False):
if 'submit' not in flask.request.form:
self.errors.append(form.ValidationError("Missing submit information."))
return
submit = flask.request.form.get('submit')
if not skip_bind:
self.bind(flask.request.form, flask.request.files)
fieldset = self.submitted_fieldset(submit)
if fieldset is not None:
if submit.endswith('.order.up') or submit.endswith('.order.down'):
# one of the arrow buttons to reorder propagandapieceitems was pressed
self.process_order(submit, fieldset)
return flask.redirect('')
fieldset.handle(skip_bind=True) # execute fieldset handle, but ignore any returned redirects
return flask.redirect('') # instead reload current page to update form
else:
submit_relative = self.submit_relative(submit)
self.validate(submit_relative)
return self.process(submit_relative)
def process_order(self, submit, fieldset):
inner_fieldset = fieldset.submitted_fieldset(submit)
item = inner_fieldset['item'].form.administerable # grab PropagandaPieceItem from ProxyFieldset
if submit.endswith('.up'):
item.order_pos -= 1
elif submit.endswith('.down'):
item.order_pos += 1
flask.flash("Moved gallery item.", 'success')
item.save()
class Gallery(admin.Named):
autoform_class = GalleryForm
2024-04-19 12:04:53 +00:00
@property
def items_ordered(self):
return self.items.order_by(GalleryItem.order_pos)
class GalleryItemForm(admin.AutoForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.mode == 'edit':
self['file'] = form.File()
2024-04-19 12:04:53 +00:00
class GalleryItem(admin.Administerable):
autoform_class = GalleryItemForm
gallery = peewee.ForeignKeyField(Gallery, null=False, backref='items', verbose_name='Gallery', on_delete='CASCADE')
order_pos = peewee.IntegerField(null=False, default=0, verbose_name='Order position')
image = peewee.ForeignKeyField(Image, null=False, verbose_name='Image', backref='in_galleries', on_delete='CASCADE')
description = markdown.MarkdownTextField(null=True, verbose_name='Description')
2024-04-19 12:04:53 +00:00
def form(self, mode):
f = super().form(mode)
if self.is_in_db:
teaser = self.image.render('teaser')
if f.intro:
f.intro = teaser + f.intro
else:
f.intro = teaser
return f
upload_class_lookup = {
'file': File,
'image': Image,
'audio': Audio,
'video': Video,
}
@app.route('/upload/<string:class_key>/<string:name>/')
def serve_upload(class_key, name):
if class_key in upload_class_lookup:
cls = upload_class_lookup[class_key]
instance = cls.load(name) # this failing is handled by error_catchall
if flask.g.user or instance.published:
download = 'download' in flask.request.args
return flask.send_from_directory(f'upload/{cls.subdirectory}', instance.filename, as_attachment=download)
flask.abort(404)