Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autogenerate project preview images for Open Graph #1886

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
554070c
Added signals and handler for project data change
anishTP Sep 28, 2023
64a4f4f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2023
43b41b3
Added a column for thumbnail_images in the project model
anishTP Sep 28, 2023
e44756f
edited signal name
anishTP Sep 28, 2023
4464ee9
Merge branch 'auto-thumbnails' of https://github.com/hasgeek/funnel i…
anishTP Sep 28, 2023
99bfe37
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2023
0478091
Merge branch 'auto-thumbnails' of https://github.com/hasgeek/funnel i…
anishTP Sep 28, 2023
43a6f80
Fixed autocorrections in precommit
anishTP Sep 28, 2023
7b2c6fd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2023
e621165
Added cache-control headers to the response object
anishTP Sep 28, 2023
ae5736e
Added the autogenerated thumbnail image to db along with a new endpo…
anishTP Oct 11, 2023
1ef3bf3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 11, 2023
ca59803
Merge branch 'main' into auto-thumbnails
jace Nov 3, 2023
487c099
Save preview image and render it with its own URL
jace Nov 3, 2023
0bdcbfb
Call this `hti` as per example in docs
jace Nov 6, 2023
6c698d2
Abort duplicate jobs in the job queue
anishTP Nov 7, 2023
e6bcca7
Unit test for preview_image function
anishTP Nov 7, 2023
949a275
updated arguments to existing pytest fixture new_project
anishTP Nov 7, 2023
52e394b
changes to the template file and tests
anishTP Nov 8, 2023
774d499
simplified template script to generate huepairs for the gradient
anishTP Nov 9, 2023
0d613a0
Unit test for the preview_image render function
anishTP Nov 9, 2023
81329b2
Revised test to check database entry for preview_image column before/…
anishTP Nov 10, 2023
64d3918
add unit test to check mimetype and image resolution
anishTP Nov 10, 2023
f98aa1b
Add mimetype sniffer and update test dependencies
anishTP Nov 10, 2023
b5c7b62
Add context manager for file handling
anishTP Nov 10, 2023
699fe31
Merge branch 'main' into auto-thumbnails
jace Nov 15, 2023
59f7b5b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 15, 2023
b81bc89
Testing if test definition fails at github actions
anishTP Nov 16, 2023
81dd78c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2023
70a0b55
Optimize preview image size and template
anishTP Nov 16, 2023
bced406
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2023
2b5ef41
Merge branch 'main' into auto-thumbnails
jace Nov 20, 2023
97750a8
Merge branch 'main' into auto-thumbnails
jace Nov 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions funnel/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@
# Commentset role change signals (sends user, document)
project_role_change = app_signals.signal('project_role_change')
proposal_role_change = app_signals.signal('proposal_role_change')

project_data_change = app_signals.signal('project_data_change')
8 changes: 4 additions & 4 deletions funnel/templates/layout.html.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@
<meta property="og:url" content="{{ request.base_url }}" />
{%- endblock canonical_url %}
{%- block image_src %}
{%- if project and project.bg_image.url %}
<link rel="image_src" href="{{ project.bg_image }}" />
<meta property="og:image" content="{{ project.bg_image }}" />
<meta name="twitter:image" content="{{ project.bg_image }}" />
{%- if project and project.preview_image %}
<link rel="image_src" href="{{ project.url_for('preview_image') }}" />
<meta property="og:image" content="{{ project.url_for('preview_image') }}" />
<meta name="twitter:image" content="{{ project.url_for('preview_image') }}" />
{%- elif project and project.account.logo_url %}
<link rel="image_src" href="{{ project.account.logo_url }}" />
<meta property="og:image" content="{{ project.account.logo_url }}" />
Expand Down
114 changes: 114 additions & 0 deletions funnel/templates/preview/project.html.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
color: #1f2d3d;
font-weight: 600;
font-family: 'SegoeUI', 'Montserrat', 'Helvetica', sans-serif;
padding: 0;
margin: 0;
}

body {
width: 320px;
height: 180px
}

.main-wrapper {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: start;
justify-content: space-between;
}

.main-content {
width: -webkit-fill-available;
}

.project-title {
font-size: 1.2em;
text-align: left;
padding-bottom: 0.5rem;
font-weight: 600;
border-bottom: 0.5px solid #1f2d3d;
}

.project-tagline {
padding-top: 0.3rem;
font-size: 0.7rem;
font-weight: 300;
}
.title-wrapper{
line-height: 1.2;
text-align: left;
padding: 0.75em;
}

.organization-label{
font-size: 0.4rem;
text-transform: uppercase;
padding-bottom: 0.1rem;
}


.datetime-wrapper {
position: absolute;
font-size: 0.5rem;
left: 1.35em;
bottom: 1.2em;
gap: 4rem;
text-align: left;
display: flex;
justify-content: space-between;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can move these to a new preview.scss file under assets/sass/pages

</style>
</head>
<body>
<main class="main-wrapper" id="container">
<div class="main-content">
<div class="title-wrapper">
<h5 class="organization-label">{{project.account.title}}</h5>
<h1 class="project-title">{{ project.title }}</h1>
<div class="project-tagline">
{{ project.tagline }}
</div>
</div>
<div class="datetime-wrapper">
<p>{{ project.datelocation }}</p>
<p>{{ project.primary_venue.title }}</p>
</div>
</div>
</main>
<script>
const randomizeIndex = (count) => {
return Math.floor(count * Math.random());
};
const randomizeElements = (hueList) => {
const uniqueHuePair = new Set();
let hueCount = hueList.length;
let randomIndex;
let hue;
while (uniqueHuePair.size < 2) {
randomIndex = randomizeIndex(hueCount);
hue = hueList[randomIndex];
uniqueHuePair.add(hue);
}
return uniqueHuePair;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set doesn't allow duplicate
This can be simplified

           const randomizeElements = (hueList) => {
                const uniqueHuePair = new Set();
                let hueCount = array.length;
                let randomIndex;
                let hue;
                while (randomSelection.size < 2) {
                    randomIndex = randomizeIndex(hueCount);
                    hue = hueList[randomIndex];
                    uniqueHuePair.add(hue);
                }
                return uniqueHuePair;
            };
            
            var [hueValue1, hueValue2] = randomizeElements(hueValues)


window.addEventListener("load", () => {
var hueValues = [5, 23, 39, 48, 92, 172, 201, 220, 273, 335]
huePair = Array.from(randomizeElements(hueValues));
console.log(huePair);
var gradient = `linear-gradient(to bottom right, hsl(${huePair[0]}, 100%, 87%), hsl(${huePair[0]+(huePair[1]-huePair[0])*0.33}, 100%, 87%), hsl(${huePair[0]+(huePair[1]-huePair[0])*0.66}, 100%, 87%), hsl(${huePair[1]}, 100%, 87%))`;
document.getElementById("container").style.background = gradient;
})
Copy link
Member

@vidya-ram vidya-ram Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. Move the JS part to a new preview.js file under assets/js. Include it in webpack.config.js

</script>
</body>
</html>
1 change: 1 addition & 0 deletions funnel/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
shortlink,
siteadmin,
sitemap,
thumbnails,
ticket_event,
ticket_participant,
update,
Expand Down
22 changes: 21 additions & 1 deletion funnel/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
from json import JSONDecodeError
from types import SimpleNamespace

from flask import Response, abort, current_app, flash, render_template, request
from flask import (
Response,
abort,
current_app,
flash,
render_template,
request,
send_file,
)
from flask_babel import format_number
from markupsafe import Markup

Expand Down Expand Up @@ -456,6 +464,7 @@ def edit(self) -> ReturnView:
db.session.commit()
flash(_("Your changes have been saved"), 'info')
tag_locations.queue(self.obj.id)
project_data_change.send(self.obj)

# Find and delete draft if it exists
if self.get_draft() is not None:
Expand Down Expand Up @@ -908,5 +917,16 @@ def update_featured(self) -> ReturnView:
}
return render_redirect(get_next_url(referrer=True))

@route('preview.png')
def preview_image(self) -> Response: # pylint: disable=R1710
"""Return the project preview image."""
if self.obj.preview_image:
return send_file(
io.BytesIO(self.obj.preview_image),
as_attachment=False,
mimetype='image/png',
)
abort(404) # TODO: Return a placeholder image


ProjectView.init_app(app)
41 changes: 41 additions & 0 deletions funnel/views/thumbnails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""View for autogenerating thumbnail previews."""

from __future__ import annotations

import os
import tempfile

from flask import render_template
from html2image import Html2Image

from ..models import Project, db
from ..signals import project_data_change
from .jobs import rqjob


@project_data_change.connect
def redo_project_preview_image(project: Project) -> None:
render_project_preview_image.queue(project_id=project.id)


@rqjob()
def render_project_preview_image(project_id: int) -> None:
"""Generate a project preview image."""
project = Project.query.get(project_id)
if project is None:
return

fd, temp_filepath = tempfile.mkstemp('.png')
os.close(fd)
temp_dir, temp_filename = os.path.split(temp_filepath)
hti = Html2Image(size=(320, 180), output_path=temp_dir)
html_src = render_template('preview/project.html.jinja2', project=project)
screenshot_files = hti.screenshot(html_str=html_src, save_as=temp_filename)

if screenshot_files:
with open(screenshot_files[0], mode='rb') as file:
project.preview_image = file.read()
db.session.commit()

for each_screenshot in screenshot_files:
os.remove(each_screenshot)
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ geoip2
grapheme
greenlet<3.0 # Required for asyncio in SQLAlchemy, but version conflict in Playwright
gunicorn
html2image
html2text
httpx[http2]
icalendar
Expand Down
15 changes: 10 additions & 5 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SHA1:2820370229ca38f2e0ba469dbbfe34d0d94623b9
# SHA1:5dcd04b0376ab9e5efeb28557b7ca2d8839cf852
#
# This file is autogenerated by pip-compile-multi
# To update, run:
Expand Down Expand Up @@ -138,7 +138,7 @@ flask-caching==2.1.0
# via baseframe
flask-executor==1.0.0
# via -r requirements/base.in
flask-flatpages==0.8.1
flask-flatpages==0.8.2
# via -r requirements/base.in
flask-mailman==1.0.0
# via -r requirements/base.in
Expand Down Expand Up @@ -190,6 +190,8 @@ h2==4.1.0
# via httpx
hpack==4.0.0
# via h2
html2image==2.0.4.3
# via -r requirements/base.in
html2text==2020.1.16
# via -r requirements/base.in
html5lib==1.1
Expand Down Expand Up @@ -236,7 +238,7 @@ linkify-it-py==2.0.2
# via -r requirements/base.in
lxml==4.9.3
# via premailer
mako==1.2.4
mako==1.3.0
# via alembic
markdown==3.5.1
# via
Expand All @@ -258,7 +260,7 @@ markupsafe==2.1.3
# wtforms
marshmallow==3.20.1
# via dataclasses-json
maxminddb==2.4.0
maxminddb==2.5.1
# via geoip2
mdit-py-plugins==0.4.0
# via -r requirements/base.in
Expand Down Expand Up @@ -374,6 +376,7 @@ requests==2.31.0
# -r requirements/base.in
# baseframe
# geoip2
# html2image
# premailer
# pyvimeo
# requests-file
Expand Down Expand Up @@ -452,7 +455,7 @@ tuspy==1.0.1
# via pyvimeo
tweepy==4.14.0
# via -r requirements/base.in
twilio==8.10.0
twilio==8.10.1
# via -r requirements/base.in
types-python-dateutil==2.8.19.14
# via arrow
Expand Down Expand Up @@ -490,6 +493,8 @@ webencodings==0.5.1
# via
# bleach
# html5lib
websocket-client==1.6.4
# via html2image
werkzeug==3.0.1
# via
# -r requirements/base.in
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/views/preview_image_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from io import BytesIO

import pytest
from magic import from_buffer
from PIL import Image

from funnel.views.thumbnails import render_project_preview_image


@pytest.mark.usefixtures('project_expo2011', 'all_fixtures')
def test_preview_image_jobs(project_expo2011) -> None:
assert project_expo2011.preview_image is None
render_project_preview_image(project_id=project_expo2011.id)
assert project_expo2011.preview_image is not None


@pytest.mark.usefixtures('project_expo2011', 'all_fixtures')
def test_preview_image_size_mimeytpe(project_expo2011) -> None:
render_project_preview_image(project_id=project_expo2011.id)
with Image.open(BytesIO(project_expo2011.preview_image)) as preview_image:
assert preview_image.size == (1280, 720)
image_mimetype = from_buffer(
BytesIO(project_expo2011.preview_image).read(2048), mime=True
)
assert image_mimetype == 'image/png'
Loading