Compare commits
5 Commits
3d6259cdcd
...
f02a85dba5
Author | SHA1 | Date | |
---|---|---|---|
f02a85dba5 | |||
fabe3b5742 | |||
58221b2e20 | |||
2b54f90f05 | |||
4b639a8211 |
@ -31,10 +31,12 @@ class CommentCollection(admin.Administerable):
|
||||
# NOTE: This is essentially a copy of TagCollection.parent
|
||||
for cls in Commentable.__class_descendants__.values():
|
||||
|
||||
try:
|
||||
return cls.select().where(cls.comment_collection == self).get()
|
||||
except cls.DoesNotExist:
|
||||
pass # continue to next loop iteration
|
||||
if cls not in app.models_abstract:
|
||||
|
||||
try:
|
||||
return cls.select().where(cls.comment_collection == self).get()
|
||||
except cls.DoesNotExist:
|
||||
pass # continue to next loop iteration
|
||||
|
||||
return None
|
||||
|
||||
|
47
main.py
47
main.py
@ -535,10 +535,18 @@ class ScoredLink(admin.Administerable):
|
||||
autoform_blacklist = ['id', 'external_site_count', 'last_scrape']
|
||||
hue_range = (80,320) # hue (degrees) from 0 to .max
|
||||
|
||||
url = peewee.CharField(unique=True) # TODO: Regexp constraint
|
||||
url = peewee.CharField(unique=True) # TODO: Regexp constraint # FIXME: overrides url function
|
||||
external_site_count = peewee.IntegerField(null=True)
|
||||
last_scrape = peewee.DateTimeField(null=True, default=None)
|
||||
|
||||
explanation_external_site_count = markdown.SafeMarkdownString("""
|
||||
How many **third-party sites** the linked content loads data from.
|
||||
|
||||
This is relevant to your privacy because each of those third-party sites
|
||||
gets – at the very least – to see that you visited the linked content.
|
||||
|
||||
*Lower* is better, **0** is *ideal*.""")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -595,6 +603,9 @@ class ScoredLink(admin.Administerable):
|
||||
@property
|
||||
def external_site_counts(self):
|
||||
|
||||
# TODO: We should probably cache this
|
||||
# (and invalidate/rebuild cache at least in .save?)
|
||||
|
||||
cls = self.__class__
|
||||
|
||||
return [
|
||||
@ -620,6 +631,10 @@ class ScoredLink(admin.Administerable):
|
||||
def mean(self):
|
||||
return sum(self.external_site_counts) / float(len(self.external_site_counts))
|
||||
|
||||
@property
|
||||
def min(self):
|
||||
return min(self.external_site_counts)
|
||||
|
||||
@property
|
||||
def max(self):
|
||||
return max(self.external_site_counts)
|
||||
@ -707,7 +722,7 @@ def renderer_link_close(self, tokens, idx, options, env):
|
||||
link.save(force_insert=True)
|
||||
|
||||
if isinstance(link.external_site_count, int):
|
||||
icon = f'<img class="scoredlink-icon" src="/theme/dynamic/scoredlink/{link.id}" title="This URL loads data from {link.external_site_count} external sites (median: {link.median}, mean: {link.mean}, set size: {link.set_size})" />'
|
||||
icon = f'<img class="scoredlink-icon" src="/theme/dynamic/scoredlink/{link.id}" title="This URL loads data from {link.external_site_count} external sites (median: {link.median}, mean: {link.mean}, min {link.min}, max: {link.max}, set size: {link.set_size})" />'
|
||||
|
||||
if icon:
|
||||
return f'</a>{icon}'
|
||||
@ -743,15 +758,12 @@ class Article(LeadImageContent):
|
||||
teaser = markdown.MarkdownTextField(null=False, verbose_name='Teaser')
|
||||
text = markdown.MarkdownTextField(null=False, verbose_name='Text')
|
||||
|
||||
@FrontPage.include
|
||||
@app.expose('/project/')
|
||||
class Project(LeadImageContent):
|
||||
class LinkForm(tagging.TaggableForm):
|
||||
|
||||
title = markdown.SafeMarkdownCharField(null=False, verbose_name='Title')
|
||||
teaser = markdown.MarkdownTextField(null=False, verbose_name='Teaser')
|
||||
text = markdown.MarkdownTextField(null=False, verbose_name='Text')
|
||||
|
||||
class CuratedArtForm(tagging.TaggableForm):
|
||||
"""
|
||||
More comfortable handing for a 'link' field that's a foreign key to ScoredLink.
|
||||
Used by Project and CuratedContent.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -791,11 +803,24 @@ class CuratedArtForm(tagging.TaggableForm):
|
||||
|
||||
return super().process(submit)
|
||||
|
||||
|
||||
@FrontPage.include
|
||||
@app.expose('/project/')
|
||||
class Project(LeadImageContent):
|
||||
|
||||
autoform_class = LinkForm
|
||||
autoform_blacklist = ('id', 'created', 'comment_collection_id', 'link_id')
|
||||
|
||||
title = markdown.SafeMarkdownCharField(null=False, verbose_name='Title')
|
||||
teaser = markdown.MarkdownTextField(null=False, verbose_name='Teaser')
|
||||
text = markdown.MarkdownTextField(null=False, verbose_name='Text')
|
||||
link = peewee.ForeignKeyField(ScoredLink, null=True, verbose_name='Link', help_text='URL for the original content, if any')
|
||||
|
||||
@FrontPage.include
|
||||
@app.expose('/curated/')
|
||||
class CuratedArt(LeadImageContent):
|
||||
|
||||
autoform_class = CuratedArtForm
|
||||
autoform_class = LinkForm
|
||||
autoform_blacklist = ('id', 'created', 'comment_collection_id', 'link_id')
|
||||
|
||||
title = markdown.SafeMarkdownCharField(null=False, verbose_name='Title')
|
||||
|
@ -160,7 +160,7 @@ def renderable_inline_render(self, tokens, idx, options, env):
|
||||
# in MarkdownString.render.
|
||||
body = str(instance.render(mode))
|
||||
|
||||
# this is ahack to break renderables out of <section>s
|
||||
# this is a hack to break renderables out of <section>s
|
||||
# and avoid invalid </section></p><section>…</p>
|
||||
# any resulting "empty" <section><p></p></section> hidden via css
|
||||
return f'</p></section>{ body }<section><p>'
|
||||
|
@ -662,10 +662,14 @@ article .unpublished {
|
||||
text-decoration: none;
|
||||
background: var(--color-bg-alt);
|
||||
backdrop-filter: blur(8px);
|
||||
color: var(--color-error);
|
||||
color: var(--color-error-inactive);
|
||||
|
||||
}
|
||||
|
||||
.modal a[href="#close"]:hover {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.modal img {
|
||||
/* enable images to be shown at full scale, even if too big for viewport */
|
||||
max-width: none !important;
|
||||
@ -687,7 +691,8 @@ article .unpublished {
|
||||
background: var(--color-highlight-inactive);
|
||||
color: var(--color-highlight-contrast);
|
||||
cursor: s-resize;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
transition: background 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@ -855,15 +860,21 @@ article.renderable > .content > h6 {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
article.administerable > .content > section {
|
||||
article.administerable > .content section {
|
||||
background: hsla(0 0 5 / 90%);
|
||||
backdrop-filter: blur(8px);
|
||||
max-width: var(--distance-typographic-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--distance-main);
|
||||
margin-bottom: var(--distance-main);
|
||||
}
|
||||
|
||||
article.administerable > .content > section:has(p:empty:only-child) {
|
||||
article.administerable > .content section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
article.administerable > .content section:has(p:empty:only-child) {
|
||||
|
||||
/* this is a hack that takes care of an edge case for another hack.
|
||||
* namely, the inline_renderable and section markdown plugins have
|
||||
@ -911,6 +922,7 @@ article.gallery {}
|
||||
article.gallery .gallery-preview {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
article.gallery img.preview {
|
||||
@ -1183,13 +1195,133 @@ body > .menus .searchminiform .field-help-wrapper {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ScoredLink view */
|
||||
.scoredlink {
|
||||
max-width: var(--distance-typographic-width);
|
||||
margin: 0 auto;
|
||||
|
||||
}
|
||||
|
||||
.scoredlink ul.fields {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.scoredlink a.url {
|
||||
/*background-image: url('/theme/svg/link.svg#active');*/
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position-x: right;
|
||||
padding-right: 1.5em; /* leave enough space for background icon */
|
||||
font-size: 150%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.scoredlink .field.external-site-count {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.scoredlink .field .value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.scoredlink .field .explanation {
|
||||
font-size: 1rem;
|
||||
margin-bottom: var(--distance-main);
|
||||
}
|
||||
|
||||
/* Propaganda view */
|
||||
|
||||
.propaganda .text,
|
||||
.propaganda .pieces {
|
||||
margin-top: var(--distance-main);
|
||||
}
|
||||
|
||||
.propaganda.mode-full .propagandapiece {
|
||||
/*padding: var(--distance-main) 0;*/
|
||||
margin-bottom: var(--distance-main);
|
||||
padding-bottom: 1px; /* hack to ensure margin doesn't spill out of container */
|
||||
}
|
||||
|
||||
.propagandapiece .preview img {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.propagandapiece:nth-child(odd) {
|
||||
background: var(--color-bg-alt);
|
||||
}
|
||||
|
||||
.propagandapiece:nth-child(odd) > .content > section {
|
||||
background: transparent;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.propagandapiece:nth-child(even) {
|
||||
}
|
||||
|
||||
.propagandapiece .files {
|
||||
width: var(--distance-typographic-width);
|
||||
padding: var(--distance-main);
|
||||
margin: var(--distance-main) auto;
|
||||
}
|
||||
|
||||
.propagandapiece:nth-child(odd) .files {
|
||||
background: var(--color-bg-main);
|
||||
}
|
||||
|
||||
.propagandapiece:nth-child(even) .files {
|
||||
background: var(--color-bg-alt);
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo {
|
||||
height: 4rem; /* remove -100% y-translated filesize from height */
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo a.download {
|
||||
display: block;
|
||||
padding-left: 4.5rem;
|
||||
line-height: 4rem; /* smaller size, determines background size */
|
||||
background-image: url('/theme/svg/download.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo a.download:hover {
|
||||
background-image: url('/theme/svg/download.svg#hover');
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo a.download:active {
|
||||
background-image: url('/theme/svg/download.svg#pressed');
|
||||
}
|
||||
|
||||
.propagandapiece .files .fileinfo .filesize {
|
||||
display: block;
|
||||
margin-left: 4.5rem;
|
||||
translate: 0 -100%;
|
||||
}
|
||||
|
||||
/*.propagandapiece .files > .fileinfo {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo > * {
|
||||
display: table-cell;
|
||||
background: var(--color-highlight);
|
||||
color: var(--color-highlight-contrast);
|
||||
padding: var(--distance-minor);
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo > *:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.propagandapiece .files > .fileinfo > *:last-child {
|
||||
text-align: right;
|
||||
}*/
|
||||
|
||||
.propagandapiece .modal article,
|
||||
.propagandapiece .modal .content {
|
||||
min-height: 100vh;
|
||||
@ -1208,9 +1340,29 @@ body > .menus .searchminiform .field-help-wrapper {
|
||||
.propaganda.mode-teaser .preview {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.propaganda.mode-teaser .preview > * {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 25%;
|
||||
}
|
||||
|
||||
.propaganda.mode-teaser .preview > :first-child {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.propaganda.mode-teaser .preview > :nth-child(2),
|
||||
.propaganda.mode-teaser .preview > :nth-child(3) {
|
||||
flex-basis: 50%;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.propaganda.mode-teaser .preview .content,
|
||||
.propaganda.mode-teaser .preview article.upload,
|
||||
.propaganda.mode-teaser .preview img {
|
||||
|
55
themes/default/assets/svg/download.svg
Normal file
55
themes/default/assets/svg/download.svg
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
|
||||
<style>
|
||||
|
||||
rect {
|
||||
fill: #580;
|
||||
}
|
||||
|
||||
#hover:target rect {
|
||||
fill: url(#fill-hover);
|
||||
}
|
||||
|
||||
#pressed:target rect {
|
||||
fill: #af0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<g id="hover">
|
||||
<g id="pressed">
|
||||
|
||||
<mask id="shape">
|
||||
<path
|
||||
fill="white"
|
||||
d="m 20.04638,29.992405 h -9.963128 c -2.083252,0 -3.083252,2 -0.03436,5.048888 L 45.007595,70 C 50,74.992405 50,74.992405 55.045878,69.946527 L 90.032052,34.960353 C 93,31.992405 92,29.992405 90.019847,29.992405 H 80 v -15 c 0,-5 -5,-10 -10,-10 H 10 c 5,0 10,5 10,10"
|
||||
id="path1" />
|
||||
<path
|
||||
fill="white"
|
||||
d="m 5,95 h 90 c 5,0 5,-15 -10,-15 H 15 C 0,80 0,95 5,95 Z"
|
||||
id="path3" />
|
||||
</mask>
|
||||
|
||||
<linearGradient id="fill-hover" x1="0" x2="0" y1="-100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#580" />
|
||||
<stop offset="30%" stop-color="#af0" />
|
||||
<stop offset="60%" stop-color="#580" />
|
||||
<animate attributeName="y1" dur="2000ms" from="-100%" to="100%" repeatCount="indefinite" />
|
||||
<animate attributeName="y2" dur="2000ms" from="100%" to="200%" repeatCount="indefinite" />
|
||||
</linearGradient>
|
||||
|
||||
<rect x="0" y="0" width="100" height="100" mask="url(#shape)" />
|
||||
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -17,6 +17,8 @@
|
||||
{% if mode == 'full' and content.comment_enabled %}
|
||||
<div class="comments">
|
||||
|
||||
<h2>Comments</h2>
|
||||
|
||||
{{ content.comment_form().render() }}
|
||||
|
||||
{% for comment in content.comments %}
|
||||
|
11
themes/default/templates/renderable/curatedart-full.html
Normal file
11
themes/default/templates/renderable/curatedart-full.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'default/templates/renderable/leadimagecontent.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% if content.link %}
|
||||
{{ content.link.render() }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
11
themes/default/templates/renderable/project-full.html
Normal file
11
themes/default/templates/renderable/project-full.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'default/templates/renderable/leadimagecontent.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% if content.link %}
|
||||
{{ content.link.render() }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -9,6 +9,7 @@
|
||||
{{ piece.render('inline-teaser') }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ content.teaser.render() }}
|
||||
|
||||
</a>
|
||||
|
@ -2,10 +2,14 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ content.text.render() }}
|
||||
<div class="text">
|
||||
{{ content.text.render() }}
|
||||
</div>
|
||||
|
||||
{% for piece in content.pieces_ordered %}
|
||||
{{ piece.render('full') }}
|
||||
{% endfor %}
|
||||
<div class="pieces">
|
||||
{% for piece in content.pieces_ordered %}
|
||||
{{ piece.render('full') }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -23,7 +23,7 @@
|
||||
{% for item in content.items_ordered[1:] %}
|
||||
{% if item.upload %}
|
||||
<div class="fileinfo">
|
||||
<a href="{{ item.upload.url('raw') }}" target="_blank">{{ item.upload.filename }}</a>
|
||||
<a class="download" href="{{ item.upload.url('download') }}" target="_blank">{{ item.upload.filename }}</a>
|
||||
<span class="filesize">{{ item.upload.filesize|filesizeformat }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -1,9 +1,35 @@
|
||||
{% extends 'default/templates/renderable/administerable.html' %}
|
||||
|
||||
{% block content %}
|
||||
<ul>
|
||||
<li class="url">URL: {{ content.url }}</li>
|
||||
<li class="external-site-count">External site count: {{ content.external_site_count }}</li>
|
||||
<li class="last-scrape">Last scrape: {{ content.last_scrape or 'never' }}</li>
|
||||
</ul>
|
||||
<a class="url" href="{{ content.url }}" style="background-image: url('/theme/dynamic/scoredlink/{{ content.id }}');">{{ content.url }}</a>
|
||||
<ul class="fields">
|
||||
<li class="field external-site-count">
|
||||
|
||||
<span class="name">External site count</span>
|
||||
<span class="value" style="color: {{ content.color.css() }};">{{ content.external_site_count }}</span>
|
||||
|
||||
<input id="scoredlink-{{ content.id }}-external-site-count-toggle" class="toggle-input" type="checkbox" />
|
||||
<label for="scoredlink-{{ content.id }}-external-site-count-toggle" class="toggle-label" title="{{ content.__class__.explanation_external_site_count }}">?</label>
|
||||
<div class="explanation">
|
||||
|
||||
{{ content.__class__.explanation_external_site_count.render() }}
|
||||
|
||||
<span>The statistical spread of our dataset currently looks like this:</span>
|
||||
<ul class="statistic-info">
|
||||
<li class="stat"><span>Median:</span> <span class="value">{{ content.median }}</span></li>
|
||||
<li class="stat"><span>Mean:</span> <span class="value">{{ content.mean }}</span></li>
|
||||
<li class="stat"><span>Min:</span> <span class="value">{{ content.min }}</span></li>
|
||||
<li class="stat"><span>Max:</span> <span class="value">{{ content.max }}</span></li>
|
||||
<li class="stat"><span>Set size:</span> <span class="value">{{ content.set_size }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="field last-scrape">
|
||||
<span class="name">Last scrape</span>
|
||||
<span class="value">{{ content.last_scrape|prettydate or 'never' }}</span>
|
||||
</li>
|
||||
|
||||
</dl>
|
||||
{% endblock %}
|
||||
|
15
upload.py
15
upload.py
@ -113,8 +113,14 @@ class Upload(admin.Named):
|
||||
|
||||
def url(self, mode='raw'):
|
||||
|
||||
if mode == 'raw':
|
||||
return f'/upload/{self.subdirectory}/{self.name}/' # FIXME: make this less hacky, if possible
|
||||
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)
|
||||
|
||||
@ -291,6 +297,9 @@ def serve_upload(class_key, name):
|
||||
instance = cls.load(name) # this failing is handled by error_catchall
|
||||
|
||||
if flask.g.user or instance.published:
|
||||
return flask.send_from_directory(f'upload/{cls.subdirectory}', instance.filename)
|
||||
|
||||
download = 'download' in flask.request.args
|
||||
|
||||
return flask.send_from_directory(f'upload/{cls.subdirectory}', instance.filename, as_attachment=download)
|
||||
|
||||
flask.abort(404)
|
||||
|
7
util.py
7
util.py
@ -186,14 +186,11 @@ class Color(object):
|
||||
self.alpha
|
||||
)
|
||||
|
||||
def css_rgb(self):
|
||||
return f'rgb({self.red * 100}% {self.green * 100}% {self.blue * 100}%)'
|
||||
|
||||
def css_rgba(self):
|
||||
def css(self):
|
||||
"""
|
||||
newstyle rgb(), NOT rgba().
|
||||
"""
|
||||
return f'rgb({self.red * 100}% {self.green * 100}% {self.blue * 100}%) / {self.alpha * 100}%'
|
||||
return f'rgb({round(self.red * 255)} {round(self.green * 255)} {round(self.blue * 255)} / {self.alpha * 100}%)'
|
||||
|
||||
def clone(self):
|
||||
return Color(red=self.red, green=self.green, blue=self.blue, alpha=self.alpha)
|
||||
|
Loading…
Reference in New Issue
Block a user