initial commit
This commit is contained in:
commit
b7bc244f86
12
.buildconfig
Normal file
12
.buildconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[default]
|
||||||
|
name=Default
|
||||||
|
runtime=podman:340af3d12b0a26614807836780c2a5760e6ea9fcb912c2f800f618bd17c0464d
|
||||||
|
toolchain=default
|
||||||
|
config-opts=
|
||||||
|
run-opts=
|
||||||
|
prefix=/var/home/valentinw/Projects/.gnome-builder/projects/thewall-project/install/podman-340af3d12b0a26614807836780c2a5760e6ea9fcb912c2f800f618bd17c0464d
|
||||||
|
app-id=
|
||||||
|
postbuild=
|
||||||
|
prebuild=
|
||||||
|
run-command=
|
||||||
|
default=true
|
||||||
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Online Streetart Museum - The Wall
|
||||||
|
|
||||||
|
Sourcecode for www.thewall.org, the online museum for stickers and graffiti.
|
||||||
|
Written in Python using aiohttp, sqlalchemy and other libraries.
|
||||||
15
config/thewall.yml
Normal file
15
config/thewall.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# database settings
|
||||||
|
postgres:
|
||||||
|
database:
|
||||||
|
user:
|
||||||
|
password:
|
||||||
|
host:
|
||||||
|
port:
|
||||||
|
|
||||||
|
# login data for the initial admin level curator
|
||||||
|
admin:
|
||||||
|
name: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
|
# name used for the session cookies
|
||||||
|
cookie_name: THEWALL
|
||||||
3
database.sql
Normal file
3
database.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
CREATE DATABASE thewall;
|
||||||
|
CREATE USER thewall_user WITH PASSWORD 'thewall_pass';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE thewall TO thewall_user;
|
||||||
52
init_db.py
Normal file
52
init_db.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from sqlalchemy import create_engine, MetaData
|
||||||
|
|
||||||
|
from thewall.settings import config
|
||||||
|
import thewall.db as db
|
||||||
|
from thewall.helpers import get_pwd_hash
|
||||||
|
|
||||||
|
|
||||||
|
DSN = "postgresql://{user}:{password}@{host}:{port}/{database}"
|
||||||
|
|
||||||
|
def create_tables(engine):
|
||||||
|
meta = MetaData()
|
||||||
|
meta.create_all(bind=engine, tables=[db.entry, db.user])
|
||||||
|
|
||||||
|
def create_admin(engine):
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute(db.user.insert(), {
|
||||||
|
'name': config['admin']['name'],
|
||||||
|
'pwd_hash': get_pwd_hash(config['admin']['password'].encode()),
|
||||||
|
'is_mod': True,
|
||||||
|
'is_admin': True
|
||||||
|
})
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db_url = DSN.format(**config['postgres'])
|
||||||
|
engine = create_engine(db_url)
|
||||||
|
|
||||||
|
create_tables(engine)
|
||||||
|
create_admin(engine)
|
||||||
|
|
||||||
|
# conn.execute(question.insert(), [
|
||||||
|
# {'question_text': 'What\'s new?',
|
||||||
|
# 'pub_date': '2015-12-15 17:17:49.629+02'}
|
||||||
|
# ])
|
||||||
|
# conn.execute(choice.insert(), [
|
||||||
|
# {'choice_text': 'Not much', 'votes': 0, 'question_id': 1},
|
||||||
|
# {'choice_text': 'The sky', 'votes': 0, 'question_id': 1},
|
||||||
|
# {'choice_text': 'Just hacking again', 'votes': 0, 'question_id': 1},
|
||||||
|
# ])
|
||||||
|
|
||||||
|
#create first admin
|
||||||
|
# async with sa_session.begin():
|
||||||
|
# m = hashlib.sha256()
|
||||||
|
# m.update(app['config']['admin']['password'].encode())
|
||||||
|
# admin = User(
|
||||||
|
# name = app['config']['admin']['name'],
|
||||||
|
# pwd_hash = m.hexdigest(),
|
||||||
|
# is_mod = True,
|
||||||
|
# is_admin = True
|
||||||
|
# )
|
||||||
|
# sa_session.add(admin)
|
||||||
|
# await sa_session.commit()
|
||||||
28
requirements.txt
Normal file
28
requirements.txt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
aiohappyeyeballs==2.4.4
|
||||||
|
aiohttp==3.11.11
|
||||||
|
aiohttp-jinja2==1.6
|
||||||
|
aiohttp-session==2.12.1
|
||||||
|
aiohttp-things==1.1.0
|
||||||
|
aiopg==1.4.0
|
||||||
|
aiosignal==1.3.2
|
||||||
|
async-timeout==4.0.3
|
||||||
|
attrs==24.3.0
|
||||||
|
cffi==1.17.1
|
||||||
|
cryptography==44.0.0
|
||||||
|
frozenlist==1.5.0
|
||||||
|
greenlet==3.1.1
|
||||||
|
idna==3.10
|
||||||
|
injector==0.14.1
|
||||||
|
invoke==2.2.0
|
||||||
|
Jinja2==3.1.5
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
multidict==6.1.0
|
||||||
|
pillow==11.1.0
|
||||||
|
propcache==0.2.1
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
pycparser==2.22
|
||||||
|
PyYAML==6.0.2
|
||||||
|
SQLAlchemy==1.4.54
|
||||||
|
sqlalchemy-things==1.1.0
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
yarl==1.18.3
|
||||||
20
roadmap.md
Normal file
20
roadmap.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## Alpha:
|
||||||
|
- ~~Submissions validieren (keine Zeiten aus der Zukunft etc.)~~
|
||||||
|
- ~~Maximale Upload Größe~~
|
||||||
|
- Thumbnail erzeugung und caching
|
||||||
|
- config.yml
|
||||||
|
- **Admin Modus**
|
||||||
|
- **Schönes CSS**
|
||||||
|
|
||||||
|
## Beta:
|
||||||
|
- Suche
|
||||||
|
- Popups/Overlays statt mehreren Seiten
|
||||||
|
- Infinite Scroll
|
||||||
|
|
||||||
|
## 1.0:
|
||||||
|
- Profanity Filter
|
||||||
|
- Docker Container
|
||||||
|
- Docker Compose
|
||||||
|
- Postgresql
|
||||||
9
tasks.py
Normal file
9
tasks.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from invoke import task
|
||||||
|
|
||||||
|
@task
|
||||||
|
def deploy(c):
|
||||||
|
...
|
||||||
|
|
||||||
|
@task
|
||||||
|
def run(c):
|
||||||
|
c.run("python main.py")
|
||||||
0
thewall/__init__.py
Normal file
0
thewall/__init__.py
Normal file
0
thewall/__main__.py
Normal file
0
thewall/__main__.py
Normal file
BIN
thewall/__pycache__/db.cpython-313.pyc
Normal file
BIN
thewall/__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
thewall/__pycache__/routes.cpython-313.pyc
Normal file
BIN
thewall/__pycache__/routes.cpython-313.pyc
Normal file
Binary file not shown.
BIN
thewall/__pycache__/settings.cpython-313.pyc
Normal file
BIN
thewall/__pycache__/settings.cpython-313.pyc
Normal file
Binary file not shown.
BIN
thewall/__pycache__/views.cpython-313.pyc
Normal file
BIN
thewall/__pycache__/views.cpython-313.pyc
Normal file
Binary file not shown.
64
thewall/db.py
Normal file
64
thewall/db.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from sqlalchemy import *
|
||||||
|
import aiopg
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['entry', 'user']
|
||||||
|
|
||||||
|
meta = MetaData()
|
||||||
|
|
||||||
|
class entry_type(enum.Enum):
|
||||||
|
Sticker = "Sticker"
|
||||||
|
Tag = "Tag"
|
||||||
|
Throwup = "Throwup"
|
||||||
|
|
||||||
|
entry = Table(
|
||||||
|
'entries', meta,
|
||||||
|
|
||||||
|
Column('id', Integer, primary_key=True),
|
||||||
|
Column('filename',String),
|
||||||
|
Column('type', Enum(entry_type)),
|
||||||
|
Column('description', String),
|
||||||
|
Column('information', String),
|
||||||
|
Column('place_found', String),
|
||||||
|
Column('time_found', Date),
|
||||||
|
Column('time_uploaded', DateTime),
|
||||||
|
Column('uploaded_by', String),
|
||||||
|
Column('approved', Boolean),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = Table(
|
||||||
|
'users', meta,
|
||||||
|
|
||||||
|
Column('id', Integer, primary_key=True).
|
||||||
|
Column('name', String).
|
||||||
|
Column('pwd_hash', String),
|
||||||
|
Column('is_mod', Boolean),
|
||||||
|
Column('is_admin', Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
|
user_sessions = Table(
|
||||||
|
'user_sessions', meta,
|
||||||
|
|
||||||
|
id = Column('id', Integer, primary_key=True),
|
||||||
|
user_id = Column('user_id', ForeignKey("users.id"))
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pg_context(app):
|
||||||
|
conf = app['config']['postgres']
|
||||||
|
engine = await aiopg.sa.create_engine(
|
||||||
|
database=conf['database'],
|
||||||
|
user=conf['user'],
|
||||||
|
password=conf['password'],
|
||||||
|
host=conf['host'],
|
||||||
|
port=conf['port'],
|
||||||
|
minsize=conf['minsize'],
|
||||||
|
maxsize=conf['maxsize'],
|
||||||
|
)
|
||||||
|
app['db'] = engine
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
app['db'].close()
|
||||||
|
await app['db'].wait_closed()
|
||||||
6
thewall/helpers.py
Normal file
6
thewall/helpers.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import hashlib
|
||||||
|
|
||||||
|
def get_pwd_hash(pwd_string):
|
||||||
|
m = hashlib.sha256()
|
||||||
|
m.update(pwd_string)
|
||||||
|
return m.hexdigest()
|
||||||
40
thewall/main.py
Executable file
40
thewall/main.py
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
import pathlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
from aiohttp import web
|
||||||
|
from cryptography import fernet
|
||||||
|
|
||||||
|
import aiohttp_jinja2 as ahj2
|
||||||
|
import aiohttp_session as ahse
|
||||||
|
|
||||||
|
from settings import config
|
||||||
|
from db import pg_context
|
||||||
|
from routes import setup_routes
|
||||||
|
|
||||||
|
###############
|
||||||
|
# Run the app #
|
||||||
|
###############
|
||||||
|
|
||||||
|
async def app_factory():
|
||||||
|
app = web.Application()
|
||||||
|
|
||||||
|
app['config'] = config
|
||||||
|
|
||||||
|
fernet_key = fernet.Fernet.generate_key()
|
||||||
|
secret_key = base64.urlsafe_b64decode(fernet_key)
|
||||||
|
#TODO: use encrypted cookie storage
|
||||||
|
ahse.setup(app, ahse.SimpleCookieStorage(cookie_name=app['config']['cookie_name']))
|
||||||
|
#ahse.setup(app, ahse.EncryptedCookieStorage(secret_key, cookie_name=app['config']['cookie_name']))
|
||||||
|
|
||||||
|
setup_routes(app)
|
||||||
|
|
||||||
|
app.cleanup_ctx.append(pg_context)
|
||||||
|
|
||||||
|
ahj2.setup(app,
|
||||||
|
loader=jinja2.FileSystemLoader(f'{pathlib.Path(__file__).parent}/templates/'))
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
web.run_app(app_factory())
|
||||||
18
thewall/routes.py
Normal file
18
thewall/routes.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import pathlib
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from views import *
|
||||||
|
|
||||||
|
def setup_routes(app):
|
||||||
|
app.add_routes([
|
||||||
|
web.get('/', wall, name='index'),
|
||||||
|
web.get('/submission', submission_form),
|
||||||
|
web.post('/submission', handle_submission),
|
||||||
|
web.get('/login', login_form),
|
||||||
|
web.post('/login', handle_login),
|
||||||
|
web.get('/about', about),
|
||||||
|
web.get(r'/entry/{id:\d+}', entry),
|
||||||
|
web.post(r'/entry/{id:\d+}', handle_edit),
|
||||||
|
web.static('/static', f'{pathlib.Path(__file__).parent}/static/'),
|
||||||
|
])
|
||||||
12
thewall/settings.py
Normal file
12
thewall/settings.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import pathlib
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
BASE_DIR = pathlib.Path(__file__).parent.parent
|
||||||
|
config_path = BASE_DIR / 'config' / 'thewall.yml'
|
||||||
|
|
||||||
|
def get_config(path):
|
||||||
|
with open(path) as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
return config
|
||||||
|
|
||||||
|
config = get_config(config_path)
|
||||||
11
thewall/static/helpers.js
Normal file
11
thewall/static/helpers.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
function displayOverlay(html) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySubmissionForm() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySticker(Id) {
|
||||||
|
|
||||||
|
}
|
||||||
BIN
thewall/static/logo1.png
Normal file
BIN
thewall/static/logo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
109
thewall/static/logo1.svg
Normal file
109
thewall/static/logo1.svg
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="309.60699mm"
|
||||||
|
height="90.546783mm"
|
||||||
|
viewBox="0 0 309.60699 90.546781"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
sodipodi:docname="logo1.svg"
|
||||||
|
inkscape:export-filename="logo1.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.1893044"
|
||||||
|
inkscape:cx="636.92693"
|
||||||
|
inkscape:cy="138.73656"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1371"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(54.606487,-125.71584)">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1"
|
||||||
|
width="23.920801"
|
||||||
|
height="27.409267"
|
||||||
|
x="-54.566895"
|
||||||
|
y="125.75543" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1-1"
|
||||||
|
width="23.920801"
|
||||||
|
height="27.409267"
|
||||||
|
x="-18.262875"
|
||||||
|
y="125.75584" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1-7"
|
||||||
|
width="23.920801"
|
||||||
|
height="27.409267"
|
||||||
|
x="18.041145"
|
||||||
|
y="125.75584" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1-2"
|
||||||
|
width="37.020275"
|
||||||
|
height="26.308609"
|
||||||
|
x="54.389526"
|
||||||
|
y="126.30615" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1-72"
|
||||||
|
width="25.29184"
|
||||||
|
height="27.2801"
|
||||||
|
x="103.83738"
|
||||||
|
y="125.82042" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1-26"
|
||||||
|
width="23.920801"
|
||||||
|
height="27.409267"
|
||||||
|
x="141.51244"
|
||||||
|
y="125.75584" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none"
|
||||||
|
id="rect1-10"
|
||||||
|
width="23.920984"
|
||||||
|
height="27.409252"
|
||||||
|
x="177.81647"
|
||||||
|
y="125.75589" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:41.6789px;font-family:'Protest Guerrilla';-inkscape-font-specification:'Protest Guerrilla';text-align:center;letter-spacing:10.5833px;text-anchor:middle;stroke:none;stroke-width:1.95369;stroke-linecap:round;stroke-linejoin:bevel;paint-order:fill markers stroke"
|
||||||
|
x="63.3484"
|
||||||
|
y="115.64455"
|
||||||
|
id="text11176"
|
||||||
|
transform="matrix(1.1266111,0,0,0.88761774,0.60699319,50.000001)"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan11174"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Protest Guerrilla';-inkscape-font-specification:'Protest Guerrilla';letter-spacing:10.5833px;fill:#ffffff;fill-opacity:1;stroke-width:1.95369"
|
||||||
|
x="68.640053"
|
||||||
|
y="115.64455"
|
||||||
|
dx="0">THEWALL</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
32
thewall/static/style.css
Normal file
32
thewall/static/style.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Verdana, Tahoma, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #fff;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
margin: 10px;
|
||||||
|
max-width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 2px solid rgb(15, 12, 7);
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 60%;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container hr {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
13
thewall/templates/about.html
Normal file
13
thewall/templates/about.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h2>About</h2>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
This Website is a collection of the many stickers, tags and other unpermitted streetart
|
||||||
|
our curators and volunteers can find.
|
||||||
|
The collection is moderated and we won't show any works that spread hatespeech
|
||||||
|
or violate any other content rules.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
28
thewall/templates/base.html
Normal file
28
thewall/templates/base.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<title>The Wall</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta lang="eng">
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<script src="/static/helpers.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Wall</a>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
<a href="/submission">Submit</a>
|
||||||
|
<!-- <button onclick="displaySubmissionForm()">Submit</button> -->
|
||||||
|
<a href="/login">Login</a>
|
||||||
|
</nav>
|
||||||
|
<div class="wrapper">
|
||||||
|
<h1>The Wall</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="wrapper">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<!-- Copyright with autodate -->
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
15
thewall/templates/entry.html
Normal file
15
thewall/templates/entry.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div id="sticker">
|
||||||
|
<img src="/images/{{ entry.id }}/{{ entry.filename }}" alt="{{ entry.description }}">
|
||||||
|
<p>Type: {{ entry.type.value }}</p>
|
||||||
|
<p>Description:</p>
|
||||||
|
<p>{{ entry.description }}</p>
|
||||||
|
<p>Information:</p>
|
||||||
|
<p>{{ entry.information }}</p>
|
||||||
|
<p>Found in: {{ entry.place_found }}</p>
|
||||||
|
<p>Found on: {{ entry.time_found }}</p>
|
||||||
|
<p>Uploaded at: {{ entry.time_uploaded }}</p>
|
||||||
|
<p>Uploaded by: {{ entry.uploaded_by }}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
17
thewall/templates/login.html
Normal file
17
thewall/templates/login.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<hr>
|
||||||
|
<form action="/login" method="post" accept-charset="utf-8" enctype="multipart/form-data">
|
||||||
|
<label for="name">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" value="">
|
||||||
|
<br>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
<br>
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
<p>By logging in you allow us the use of cookies.</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
14
thewall/templates/sticker.html
Normal file
14
thewall/templates/sticker.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div id="sticker">
|
||||||
|
<img src="/images/{{ entry.id }}/{{ entry.filename }}" alt="{{ entry.description }}">
|
||||||
|
<p>Type: {{ entry.type.value }}</p>
|
||||||
|
<p>Description:</p>
|
||||||
|
<p>{{ entry.description }}</p>
|
||||||
|
<p>Information:</p>
|
||||||
|
<p>{{ entry.information }}</p>
|
||||||
|
<p>Found in: {{ entry.place_found }}</p>
|
||||||
|
<p>Found on: {{ entry.time_found }}</p>
|
||||||
|
<p>Uploaded at: {{ entry.time_uploaded }}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
43
thewall/templates/submission.html
Normal file
43
thewall/templates/submission.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h2>Submission</h2>
|
||||||
|
<hr>
|
||||||
|
<form action="/submission" method="post" accept-charset="utf-8" enctype="multipart/form-data">
|
||||||
|
{% if submission_successful %}
|
||||||
|
<p>Your submission will be reviewed.</p>
|
||||||
|
{% elif submission_successful == False %}
|
||||||
|
<p>{{ error_message }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<label for="image">Select an Image:</label>
|
||||||
|
<br>
|
||||||
|
<input type="file" id="image" name="image" value=""/>
|
||||||
|
<br><br>
|
||||||
|
<label for="type">Content type:</label>
|
||||||
|
<select id="type" name="type">
|
||||||
|
<option value="Sticker">Sticker</option>
|
||||||
|
<option value="Tag">Tag</option>
|
||||||
|
<option value="Throwup">Throwup</option>
|
||||||
|
</select>
|
||||||
|
<br><br>
|
||||||
|
<label for="description">Description:</label>
|
||||||
|
<br>
|
||||||
|
<textarea id="description" name="description" value=""></textarea>
|
||||||
|
<br><br>
|
||||||
|
<label for="information">Information:</label>
|
||||||
|
<br>
|
||||||
|
<textarea id="information" name="information" value=""></textarea>
|
||||||
|
<br><br>
|
||||||
|
<label for="place">Found at:</label>
|
||||||
|
<input type="text" id="place" name="place" value="">
|
||||||
|
<br><br>
|
||||||
|
<label for="date">Found on:</label>
|
||||||
|
<input type="date" id="date" name="date" value="">
|
||||||
|
<br><br>
|
||||||
|
<label for="place">Username:</label>
|
||||||
|
<input type="text" id="name" name="name" value="Anonymous">
|
||||||
|
<br><br>
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
14
thewall/templates/wall.html
Normal file
14
thewall/templates/wall.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wall_row">
|
||||||
|
{% for column in columns %}
|
||||||
|
<div class="wall_column">
|
||||||
|
{% for entry in column %}
|
||||||
|
<a href="/sticker/{{ entry.id }}">
|
||||||
|
<img src="/images/{{ entry.id }}/thumbnail/{{ entry.filename }}">
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
164
thewall/views.py
Normal file
164
thewall/views.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
import aiohttp_jinja2 as ahj2
|
||||||
|
import aiohttp_session as ahse
|
||||||
|
|
||||||
|
import db
|
||||||
|
from helpers import get_pwd_hash
|
||||||
|
|
||||||
|
@ahj2.template('wall.html')
|
||||||
|
async def wall(request):
|
||||||
|
#TODO: render wall
|
||||||
|
async with request.app['db'].acquire() as conn:
|
||||||
|
...
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
@ahj2.template('entry.html')
|
||||||
|
async def entry(request):
|
||||||
|
async with request.app['db'].acquire() as conn:
|
||||||
|
cursor = await conn.execute(db.entry.select().where(db.entry.id == request.match_info['id']))
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
# sa_session = ahsa.get_session(request)
|
||||||
|
# async with sa_session.begin():
|
||||||
|
# stmt = sa.select(entries).where(entries.id == request.match_info['id'])
|
||||||
|
# result = await sa_session.scalars(stmt)
|
||||||
|
return {'entry': records.all()[0]}
|
||||||
|
|
||||||
|
@ahj2.template('entry.html')
|
||||||
|
async def handle_edit(request):
|
||||||
|
#TODO
|
||||||
|
async with request.app['db'].acquire() as conn:
|
||||||
|
cursor = await conn.execute(db.entry.select().where(entry.id == request.match_info['id']))
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
# sa_session = ahsa.get_session(request)
|
||||||
|
# async with sa_session.begin():
|
||||||
|
# stmt = sa.select(Entry).where(Entry.id == request.match_info['id'])
|
||||||
|
# result = await sa_session.scalars(stmt)
|
||||||
|
return {'entry': records.all()[0]}
|
||||||
|
|
||||||
|
@ahj2.template('submission.html')
|
||||||
|
async def submission_form(request):
|
||||||
|
#TODO: fill in username for Mods and Admins
|
||||||
|
return
|
||||||
|
|
||||||
|
@ahj2.template('submission.html')
|
||||||
|
async def handle_submission(request):
|
||||||
|
tf = tempfile.NamedTemporaryFile(mode='w+b', delete=False)
|
||||||
|
temp_file_name = tf.name
|
||||||
|
|
||||||
|
async for field in (await request.multipart()):
|
||||||
|
if field.name == 'image':
|
||||||
|
filename = field.filename
|
||||||
|
size = 0
|
||||||
|
while True:
|
||||||
|
chunk = await field.read_chunk() # 8192 bytes by default.
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
size += len(chunk)
|
||||||
|
if size >= 2e7:
|
||||||
|
tf.close()
|
||||||
|
os.remove(temp_file_name)
|
||||||
|
return {'submission_successful': False, 'error_message': \
|
||||||
|
"Your file exceeds the maximum size of 20Mb."}
|
||||||
|
tf.write(chunk)
|
||||||
|
tf.close()
|
||||||
|
if field.name == 'type':
|
||||||
|
type = await field.read(decode=True)
|
||||||
|
if field.name == 'description':
|
||||||
|
desc = await field.read(decode=True)
|
||||||
|
if field.name == 'information':
|
||||||
|
info = await field.read(decode=True)
|
||||||
|
if field.name == 'place':
|
||||||
|
place = await field.read(decode=True)
|
||||||
|
if field.name == 'date':
|
||||||
|
date = await field.read(decode=True)
|
||||||
|
date = datetime.datetime.strptime(date.decode(), '%Y-%m-%d')
|
||||||
|
if date > datetime.datetime.now():
|
||||||
|
return {'submission_successful': False, 'error_message': \
|
||||||
|
"Due to the linear nature of time your submissions date of discovery is likely wrong."}
|
||||||
|
if field.name == 'name':
|
||||||
|
name = await field.read(decode=True)
|
||||||
|
#TODO: Tripcodes
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
id = 0
|
||||||
|
async with request.app['db'].acquire() as conn:
|
||||||
|
await conn.execute(db.entry.insert({
|
||||||
|
'filename': filename,
|
||||||
|
'type': type.decode(),
|
||||||
|
'description': desc.decode(),
|
||||||
|
'information': info.decode(),
|
||||||
|
'place_found': place.decode(),
|
||||||
|
'time_found': date,
|
||||||
|
'time_uploaded': datetime.datetime.now(),
|
||||||
|
'uploaded_by': name.decode(),
|
||||||
|
'approved': False
|
||||||
|
}))
|
||||||
|
conn.close()
|
||||||
|
# sa_session = ahsa.get_session(request)
|
||||||
|
# async with sa_session.begin():
|
||||||
|
# entry = Entry(
|
||||||
|
# filename = filename,
|
||||||
|
# type = type.decode(),
|
||||||
|
# description = desc.decode(),
|
||||||
|
# information = info.decode(),
|
||||||
|
# place_found = place.decode(),
|
||||||
|
# time_found = date,
|
||||||
|
# time_uploaded = datetime.datetime.now(),
|
||||||
|
# uploaded_by = name.decode(),
|
||||||
|
# approved = False)
|
||||||
|
# sa_session.add(entry)
|
||||||
|
# await sa_session.commit()
|
||||||
|
# id = str(entry.id)
|
||||||
|
|
||||||
|
pathlib.Path(f"./images/{id}/thumbnail").mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy(temp_file_name, f"./images/{id}/{filename}")
|
||||||
|
os.remove(temp_file_name)
|
||||||
|
|
||||||
|
size = (200, 200)
|
||||||
|
with Image.open(f"./images/{id}/{filename}") as im:
|
||||||
|
ImageOps.contain(im, size).save(f"./images/{id}/thumbnail/{filename}")
|
||||||
|
|
||||||
|
return {'submission_successful': True}
|
||||||
|
|
||||||
|
@ahj2.template('login.html')
|
||||||
|
async def login_form(request):
|
||||||
|
return
|
||||||
|
|
||||||
|
@ahj2.template('login.html')
|
||||||
|
async def handle_login(request):
|
||||||
|
|
||||||
|
async for field in (await request.multipart()):
|
||||||
|
if field.name == 'username':
|
||||||
|
name = await field.read(decode=True)
|
||||||
|
if field.name == 'password':
|
||||||
|
pwd = await field.read(decode=True)
|
||||||
|
|
||||||
|
pwd_hash = get_pwd_hash(pwd)
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
async with request.app['db'].acquire() as conn:
|
||||||
|
cursor = await conn.execute(db.user.select().where(db.user.name == name))
|
||||||
|
conn.close()
|
||||||
|
# sa_session = await ahsa.new_session(request)
|
||||||
|
# async with sa_session.begin():
|
||||||
|
# stmt = sa.select(User).where(User.name == name)
|
||||||
|
# result = await sa_session.scalars(stmt)
|
||||||
|
# # verify result
|
||||||
|
# if result.all()[0].pwd_hash == pwd_hash:
|
||||||
|
# se_session = ahse.new_session()
|
||||||
|
# # enter data into the cookie to reidentify the user
|
||||||
|
# location = request.app.router['index'].url_for()
|
||||||
|
# raise web.HTTPFound(location=location)
|
||||||
|
# else:
|
||||||
|
# return {'valid': False}
|
||||||
|
|
||||||
|
@ahj2.template('about.html')
|
||||||
|
async def about(request):
|
||||||
|
return
|
||||||
Loading…
Reference in New Issue
Block a user