From b7bc244f86f809950da662543ab73c83a12c128a Mon Sep 17 00:00:00 2001 From: Valentin Wagner Date: Sat, 7 Feb 2026 16:34:22 +0100 Subject: [PATCH] initial commit --- .buildconfig | 12 ++ README.md | 4 + config/thewall.yml | 15 ++ database.sql | 3 + init_db.py | 52 ++++++ requirements.txt | 28 ++++ roadmap.md | 20 +++ tasks.py | 9 + thewall/__init__.py | 0 thewall/__main__.py | 0 thewall/__pycache__/db.cpython-313.pyc | Bin 0 -> 2062 bytes thewall/__pycache__/routes.cpython-313.pyc | Bin 0 -> 1125 bytes thewall/__pycache__/settings.cpython-313.pyc | Bin 0 -> 737 bytes thewall/__pycache__/views.cpython-313.pyc | Bin 0 -> 9307 bytes thewall/db.py | 64 ++++++++ thewall/helpers.py | 6 + thewall/main.py | 40 +++++ thewall/routes.py | 18 ++ thewall/settings.py | 12 ++ thewall/static/helpers.js | 11 ++ thewall/static/logo1.png | Bin 0 -> 27705 bytes thewall/static/logo1.svg | 109 ++++++++++++ thewall/static/style.css | 32 ++++ thewall/templates/about.html | 13 ++ thewall/templates/base.html | 28 ++++ thewall/templates/entry.html | 15 ++ thewall/templates/login.html | 17 ++ thewall/templates/sticker.html | 14 ++ thewall/templates/submission.html | 43 +++++ thewall/templates/wall.html | 14 ++ thewall/views.py | 164 +++++++++++++++++++ 31 files changed, 743 insertions(+) create mode 100644 .buildconfig create mode 100644 README.md create mode 100644 config/thewall.yml create mode 100644 database.sql create mode 100644 init_db.py create mode 100644 requirements.txt create mode 100644 roadmap.md create mode 100644 tasks.py create mode 100644 thewall/__init__.py create mode 100644 thewall/__main__.py create mode 100644 thewall/__pycache__/db.cpython-313.pyc create mode 100644 thewall/__pycache__/routes.cpython-313.pyc create mode 100644 thewall/__pycache__/settings.cpython-313.pyc create mode 100644 thewall/__pycache__/views.cpython-313.pyc create mode 100644 thewall/db.py create mode 100644 thewall/helpers.py create mode 100755 thewall/main.py create mode 100644 thewall/routes.py create mode 100644 thewall/settings.py create mode 100644 thewall/static/helpers.js create mode 100644 thewall/static/logo1.png create mode 100644 thewall/static/logo1.svg create mode 100644 thewall/static/style.css create mode 100644 thewall/templates/about.html create mode 100644 thewall/templates/base.html create mode 100644 thewall/templates/entry.html create mode 100644 thewall/templates/login.html create mode 100644 thewall/templates/sticker.html create mode 100644 thewall/templates/submission.html create mode 100644 thewall/templates/wall.html create mode 100644 thewall/views.py diff --git a/.buildconfig b/.buildconfig new file mode 100644 index 0000000..faddb2f --- /dev/null +++ b/.buildconfig @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6b4d67 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/config/thewall.yml b/config/thewall.yml new file mode 100644 index 0000000..a7d6cab --- /dev/null +++ b/config/thewall.yml @@ -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 \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..73131d8 --- /dev/null +++ b/database.sql @@ -0,0 +1,3 @@ +CREATE DATABASE thewall; +CREATE USER thewall_user WITH PASSWORD 'thewall_pass'; +GRANT ALL PRIVILEGES ON DATABASE thewall TO thewall_user; diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..2e2ed3a --- /dev/null +++ b/init_db.py @@ -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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f7bfe9b --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..e8c34c7 --- /dev/null +++ b/roadmap.md @@ -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 diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..85bfd5d --- /dev/null +++ b/tasks.py @@ -0,0 +1,9 @@ +from invoke import task + +@task +def deploy(c): + ... + +@task +def run(c): + c.run("python main.py") \ No newline at end of file diff --git a/thewall/__init__.py b/thewall/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/thewall/__main__.py b/thewall/__main__.py new file mode 100644 index 0000000..e69de29 diff --git a/thewall/__pycache__/db.cpython-313.pyc b/thewall/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ccf6092aac3302e4be5ea5e6ce3f94ee61959ab GIT binary patch literal 2062 zcmbVN-)q}e6qan+lBGC~^P_Q-b_+=vB(S)Qg>GeZkfu$S&@w8IZD6=4*0rNWmfRyb z!F|{aficL7ODW6V^4kAlPx}wrJS@3m4`qAF+gQB!og@Ef>iMBepmWbT_uQka@B4Ih zo=)o$Ud6u;Dlg-b^almKE75h%e@5q-WJ;#IDFKbKP5DM_j=cD$yb%NRi}87BrMm!> zV(ekmx7VsO&PzqbR6dp=nUm%vGchBX$)fad^vx=YR&XzLffiM&Sg940{cI}ArZB5t z$m&tn!0gC{tWl8{(`VG|!J;hWB{Xp#syJv>9$K~)S|yt8U2@kspF`)FRFptAC5V|a z#LXBeW*icx0!cFgY9+a-3Y~i)G+5ZE@v{^z5vmzFr5z3;Ua%@cEo_6oSFb(C$7@1k z%(JSTF`+Y7_3gS#{s?1_>XzG`$uL%SAP8N@BtFRRT9DuNs~jB{x950!`Om@shL^%1-`(Apmh0vNDCO;~v1<-CRG>GtJxR?XSalujUQB`YQPar~ivrF@dZn%{cNN12u}Yl`h7Rieeor zj9#uc!pJ~vdLSMhh%-?<)?c5E;_-ewF;G7l#Z&$HOUUSS#AxCUF?vR_6e_Xn@SyNq z7Df%6s)bzd@J0mpgT)-A2^1=Y44E-nlx8fnwh;La)ydnwTd#UT{mct_1%(lFY>_mv zuU8RT+36Gt<&IacMjQx*)Fcd>2PJT7q2qhPaJ(|g7I~+j=2|7r%6{Fmg|cRaTL zy6R*!$`mjb=_p&TxxQs{TNpi$Z8e0t>iaIYJfT^&8u+{12I4(>vkuo26i)RFy=1V1 z29+^+wR!(Em1(I*g%_!%=IUu?s#QMtwt4T=$UeE&dVD-Se{3u?*G|WChnJo%H#dhk z+37>&>6PZj>Ez7eN5_-buw%a(+f@#}+rQV@2W?@v=E^wpzclw+gL&8t2n|Qet~%aOM&73;WT+cw0b) ze%A-?RJ={z5PGQ&R_Fu6KEvcp|0p0s8wuvo5O@m%;AN+O(vNN)=a!GN`TdQK3Te^e zh)_6wra})Mxt3en=G6u$I3cwA_%p%p&H_3R#hA@Yu7x{sb~)R!0xpzQv@nO0ox_$y z9kCegX5gz6+9#dA!!7s}>*%};zDDy`T$bhEB;!vh_q#On%9uE~+0;%{{a1DNr8;}` z)eCjGskD{2oNG<~c=^fYwuEk5H{^GJSZhl-v}cm&f7q6AXphC^>n-IUthpY&;=g-W B*5m*H literal 0 HcmV?d00001 diff --git a/thewall/__pycache__/routes.cpython-313.pyc b/thewall/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d876625924a9857721a7cc275d193de29439ed27 GIT binary patch literal 1125 zcmb7DF>ljA6uyh?`0B*8r4nE6%jjU$4)v#Ej-D-_wIY&d*Ab&N4l;7VLyL=Y#c%W zendfPnZy|_3ug})z<>|HolF53nahB}hE$Y)MJYwdVW}?I6FVn7Nj#(PVs{Tb23sf5 zB+X3ggDqvkoHzzFvuR*t3~56;BVRUAn!lb@KEcSP*mN~yGoMB?7_&4fW;9uBGZ?da zDCWW`j9ERh=K23*sAhCPvHhbe)x2;D<5G%ER|6SyX>B~Wo}6X@G(b_SaEQ6$cs6+{ zZi4JtP14O_)~+=j#vI@4X0huxoM^Rbemm&SW8wvL7r%1syHD+#ue&N{ffYFQu3pBB z){_Vqb*@>qU8RCV7?(Pv#-#=cxZJT^m(NdIRG;~@$>o;M0=~FydA3WcQ{yc+NwQik7L8osxABfe@wVS2!f^%5@jCb+^`Db^z_1}^ zctPkBp)Q_dEWF&><#|Seb}N~LUKF*W--GRo*IRHXtM4oCDn}^yN$sh9>GPv6>IZd* zZlpW<;7y2d6rmpKU;S2C3(>6s1!3?*>OmR#iG9gx*~RIbbC13Z2ETFCHLqR X5k}vK>Ed-PK?sk)(n!uhHBSEvyJFb% literal 0 HcmV?d00001 diff --git a/thewall/__pycache__/settings.cpython-313.pyc b/thewall/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c548f810af67408c0b2d5a7698b58ed3552385e GIT binary patch literal 737 zcmZuv&ubGw6n?Y2*=#Z-kXnC`L%S&+60lu86g)&hu?HzFB}?d`fn`m0H_^>*oLwyu zk$91!0gF)Vu~!fNF(HS_w1Y;Don>YL3eD8a2c9hAa0qynq2QNVY zzKg@mvupeQO1JITr)9_GR7BCh=ncXF?EH;Y zVMZuHKz5{YHp1o2RLLwmkZ=IFR;M>~s7g1X0JoqxrD$LcSJGh-bX?mQy(ce%OG;$3 zBy%O~I009>cH8Hv$aWpe5AB8sbnLjviOYoCNPw>E{I2oRW=1n?JLvdM5VwL|V~d4P zoq8M@X56YjbJ#X^ZbYnZL{5weUbNEb^0eo~)?_>khY3kRdmEt#r>V?dqo3b@eww@b zdEwK-5gF!|_S6d*((`9D*W0*Apx4ij{oj#zzW()P`D?nIETwo-2=!Y}n3yeBwuEfW zvfP&MSQb}1Hba?Qd-%XyvmQOJa5@oTiECpe;|{;T2(JSZ{~g!L?XdB}ckZ)!Jc>7p z))0Qmgpd=+o&Y_gb05p^%ZJ*K7LI9gK#RxJ7*M09eal`uyfMfYjy49_J3V@?$YkzU Q8ujn54^#SEm0dxmKRM@^QUCw| literal 0 HcmV?d00001 diff --git a/thewall/__pycache__/views.cpython-313.pyc b/thewall/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba82ddfc48b01ec85ac630e762094a7debca6d87 GIT binary patch literal 9307 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 IcR@Y>1#y?oQvd(} literal 0 HcmV?d00001 diff --git a/thewall/db.py b/thewall/db.py new file mode 100644 index 0000000..480cbce --- /dev/null +++ b/thewall/db.py @@ -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() diff --git a/thewall/helpers.py b/thewall/helpers.py new file mode 100644 index 0000000..fe96661 --- /dev/null +++ b/thewall/helpers.py @@ -0,0 +1,6 @@ +import hashlib + +def get_pwd_hash(pwd_string): + m = hashlib.sha256() + m.update(pwd_string) + return m.hexdigest() \ No newline at end of file diff --git a/thewall/main.py b/thewall/main.py new file mode 100755 index 0000000..c2af319 --- /dev/null +++ b/thewall/main.py @@ -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()) diff --git a/thewall/routes.py b/thewall/routes.py new file mode 100644 index 0000000..be16862 --- /dev/null +++ b/thewall/routes.py @@ -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/'), + ]) \ No newline at end of file diff --git a/thewall/settings.py b/thewall/settings.py new file mode 100644 index 0000000..9bd0980 --- /dev/null +++ b/thewall/settings.py @@ -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) \ No newline at end of file diff --git a/thewall/static/helpers.js b/thewall/static/helpers.js new file mode 100644 index 0000000..9a2452f --- /dev/null +++ b/thewall/static/helpers.js @@ -0,0 +1,11 @@ +function displayOverlay(html) { + +} + +function displaySubmissionForm() { + +} + +function displaySticker(Id) { + +} \ No newline at end of file diff --git a/thewall/static/logo1.png b/thewall/static/logo1.png new file mode 100644 index 0000000000000000000000000000000000000000..b3114ddca3f16bca0b9b1107626914983f9250cd GIT binary patch literal 27705 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 + + + + + + + + + + + + + + THEWALL + + diff --git a/thewall/static/style.css b/thewall/static/style.css new file mode 100644 index 0000000..c006353 --- /dev/null +++ b/thewall/static/style.css @@ -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; +} \ No newline at end of file diff --git a/thewall/templates/about.html b/thewall/templates/about.html new file mode 100644 index 0000000..49c8128 --- /dev/null +++ b/thewall/templates/about.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} +
+

About

+
+

+ 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. +

+
+{% endblock content %} \ No newline at end of file diff --git a/thewall/templates/base.html b/thewall/templates/base.html new file mode 100644 index 0000000..a087a87 --- /dev/null +++ b/thewall/templates/base.html @@ -0,0 +1,28 @@ + + + The Wall + + + + + + +
+ +
+

The Wall

+
+
+
+ {% block content %}{% endblock %} +
+
+ +
+ \ No newline at end of file diff --git a/thewall/templates/entry.html b/thewall/templates/entry.html new file mode 100644 index 0000000..460e6b6 --- /dev/null +++ b/thewall/templates/entry.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block content %} +
+ {{ entry.description }} +

Type: {{ entry.type.value }}

+

Description:

+

{{ entry.description }}

+

Information:

+

{{ entry.information }}

+

Found in: {{ entry.place_found }}

+

Found on: {{ entry.time_found }}

+

Uploaded at: {{ entry.time_uploaded }}

+

Uploaded by: {{ entry.uploaded_by }}

+
+{% endblock %} \ No newline at end of file diff --git a/thewall/templates/login.html b/thewall/templates/login.html new file mode 100644 index 0000000..dd959ae --- /dev/null +++ b/thewall/templates/login.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block content %} +
+

Login

+
+
+ + +
+ + +
+ +
+

By logging in you allow us the use of cookies.

+
+{% endblock %} \ No newline at end of file diff --git a/thewall/templates/sticker.html b/thewall/templates/sticker.html new file mode 100644 index 0000000..d9ce528 --- /dev/null +++ b/thewall/templates/sticker.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block content %} +
+ {{ entry.description }} +

Type: {{ entry.type.value }}

+

Description:

+

{{ entry.description }}

+

Information:

+

{{ entry.information }}

+

Found in: {{ entry.place_found }}

+

Found on: {{ entry.time_found }}

+

Uploaded at: {{ entry.time_uploaded }}

+
+{% endblock %} \ No newline at end of file diff --git a/thewall/templates/submission.html b/thewall/templates/submission.html new file mode 100644 index 0000000..5e35c3d --- /dev/null +++ b/thewall/templates/submission.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block content %} +
+

Submission

+
+
+ {% if submission_successful %} +

Your submission will be reviewed.

+ {% elif submission_successful == False %} +

{{ error_message }}

+ {% endif %} + +
+ +

+ + +

+ +
+ +

+ +
+ +

+ + +

+ + +

+ + +

+ +
+
+{% endblock %} \ No newline at end of file diff --git a/thewall/templates/wall.html b/thewall/templates/wall.html new file mode 100644 index 0000000..3e380c2 --- /dev/null +++ b/thewall/templates/wall.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block content %} +
+ {% for column in columns %} +
+ {% for entry in column %} + + + + {% endfor %} +
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/thewall/views.py b/thewall/views.py new file mode 100644 index 0000000..5f31f9c --- /dev/null +++ b/thewall/views.py @@ -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 \ No newline at end of file