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