2024-04-14 14:32:40 +00:00
|
|
|
# builtins
|
|
|
|
import os
|
|
|
|
import mimetypes
|
|
|
|
|
|
|
|
# third-party
|
2024-05-21 03:58:28 +00:00
|
|
|
import markupsafe
|
2024-04-14 14:32:40 +00:00
|
|
|
import werkzeug
|
|
|
|
import flask
|
|
|
|
import peewee
|
|
|
|
|
|
|
|
# internals
|
|
|
|
from application import app
|
|
|
|
|
|
|
|
import form
|
2024-04-17 17:30:52 +00:00
|
|
|
import database
|
2024-04-14 14:32:40 +00:00
|
|
|
import admin
|
2024-04-14 16:00:32 +00:00
|
|
|
import markdown
|
2024-04-14 14:32:40 +00:00
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
@app.abstract
|
2024-04-14 14:32:40 +00:00
|
|
|
class Upload(admin.Named):
|
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
subdirectory = None
|
2024-04-14 14:32:40 +00:00
|
|
|
published = peewee.BooleanField(null=False, default=False, verbose_name='Published')
|
|
|
|
filename = peewee.CharField(null=False, unique=True)
|
2024-06-15 14:19:08 +00:00
|
|
|
description = markdown.MarkdownTextField(null=True)
|
2024-04-14 14:32:40 +00:00
|
|
|
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
|
|
|
|
|
2024-05-21 03:58:28 +00:00
|
|
|
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)
|
|
|
|
|
2024-04-29 16:16:00 +00:00
|
|
|
def form(self, mode, **kwargs):
|
2024-04-14 14:32:40 +00:00
|
|
|
|
2024-04-29 16:16:00 +00:00
|
|
|
f = super().form(mode, **kwargs)
|
2024-04-14 14:32:40 +00:00
|
|
|
|
|
|
|
if self.is_in_db:
|
|
|
|
|
2024-05-05 22:15:55 +00:00
|
|
|
teaser = self.render('inline')
|
2024-04-14 14:32:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
return form.File(label=field_label, value=field_value, help=field_help, required=field_required)
|
2024-04-14 14:32:40 +00:00
|
|
|
|
|
|
|
return super().form_field(field_name)
|
|
|
|
|
|
|
|
def form_field_handle(self, field, mode):
|
|
|
|
|
|
|
|
if field.name == 'filename':
|
|
|
|
|
|
|
|
if field.valid and field.value:
|
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
self.write_file(field.value)
|
|
|
|
flask.flash("Stored new file.", 'info')
|
|
|
|
|
|
|
|
def write_file(self, file: werkzeug.datastructures.file_storage.FileStorage):
|
2024-04-14 14:32:40 +00:00
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
extension = file.filename.split('.')[-1]
|
|
|
|
filename = f'{self.name}.{extension}'
|
|
|
|
filename_clean = werkzeug.utils.secure_filename(filename)
|
2024-04-14 14:32:40 +00:00
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
if self.filename:
|
2024-04-14 14:32:40 +00:00
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
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.")
|
2024-04-14 14:32:40 +00:00
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
self.filename = filename_clean
|
|
|
|
file.save(self.path)
|
2024-04-14 14:32:40 +00:00
|
|
|
|
|
|
|
def url(self, mode='raw'):
|
|
|
|
|
2024-09-01 16:50:56 +00:00
|
|
|
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
|
2024-04-14 14:32:40 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2024-05-31 14:41:36 +00:00
|
|
|
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'
|
|
|
|
|
2024-04-14 14:32:40 +00:00
|
|
|
class Image(Upload):
|
|
|
|
|
|
|
|
subdirectory = 'image'
|
|
|
|
|
|
|
|
class Audio(Upload):
|
|
|
|
|
|
|
|
subdirectory = 'audio'
|
|
|
|
|
|
|
|
class Video(Upload):
|
|
|
|
|
|
|
|
subdirectory = 'video'
|
|
|
|
|
2024-04-17 17:30:52 +00:00
|
|
|
class GalleryForm(admin.AutoForm):
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
if self.mode == 'edit':
|
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
self['items'] = form.Fieldset()
|
|
|
|
|
2024-04-21 14:02:58 +00:00
|
|
|
for item in self.administerable.items_ordered:
|
2024-04-17 17:30:52 +00:00
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
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='⌄')
|
2024-04-17 17:30:52 +00:00
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
# 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
|
|
|
|
2024-04-17 17:30:52 +00:00
|
|
|
fieldset = form.ProxyFieldset(GalleryItem().form('create'), name='item-new')
|
2024-05-31 14:41:36 +00:00
|
|
|
fieldset['gallery_id'] = form.ValueField(value=self.administerable.id)
|
2024-07-13 21:09:48 +00:00
|
|
|
del(fieldset['order_pos'])
|
|
|
|
|
|
|
|
self['items']['new'] = fieldset
|
2024-04-17 17:30:52 +00:00
|
|
|
|
|
|
|
def handle(self, skip_bind=False):
|
|
|
|
|
|
|
|
if 'submit' not in flask.request.form:
|
2024-05-31 14:41:36 +00:00
|
|
|
self.errors.append(form.ValidationError("Missing submit information."))
|
2024-04-17 17:30:52 +00:00
|
|
|
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:
|
2024-07-13 21:09:48 +00:00
|
|
|
|
|
|
|
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('')
|
|
|
|
|
2024-04-17 17:30:52 +00:00
|
|
|
fieldset.handle(skip_bind=True) # execute fieldset handle, but ignore any returned redirects
|
|
|
|
return flask.redirect('') # instead reload current page to update form
|
2024-07-13 21:09:48 +00:00
|
|
|
|
2024-04-17 17:30:52 +00:00
|
|
|
else:
|
|
|
|
submit_relative = self.submit_relative(submit)
|
|
|
|
self.validate(submit_relative)
|
|
|
|
return self.process(submit_relative)
|
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
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()
|
|
|
|
|
2024-04-17 17:30:52 +00:00
|
|
|
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)
|
2024-04-17 17:30:52 +00:00
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
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):
|
2024-04-17 17:30:52 +00:00
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
autoform_class = GalleryItemForm
|
|
|
|
|
2024-04-21 14:02:58 +00:00
|
|
|
gallery = peewee.ForeignKeyField(Gallery, null=False, backref='items', verbose_name='Gallery', on_delete='CASCADE')
|
2024-06-29 03:36:26 +00:00
|
|
|
order_pos = peewee.IntegerField(null=False, default=0, verbose_name='Order position')
|
2024-04-21 14:02:58 +00:00
|
|
|
image = peewee.ForeignKeyField(Image, null=False, verbose_name='Image', backref='in_galleries', on_delete='CASCADE')
|
2024-06-15 14:19:08 +00:00
|
|
|
description = markdown.MarkdownTextField(null=True, verbose_name='Description')
|
2024-04-17 17:30:52 +00:00
|
|
|
|
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
|
|
|
|
|
2024-04-14 14:32:40 +00:00
|
|
|
upload_class_lookup = {
|
2024-05-31 14:41:36 +00:00
|
|
|
'file': File,
|
2024-04-14 14:32:40 +00:00
|
|
|
'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:
|
2024-09-01 16:50:56 +00:00
|
|
|
|
|
|
|
download = 'download' in flask.request.args
|
|
|
|
|
|
|
|
return flask.send_from_directory(f'upload/{cls.subdirectory}', instance.filename, as_attachment=download)
|
2024-04-14 14:32:40 +00:00
|
|
|
|
|
|
|
flask.abort(404)
|