First-term project for Advanced Databases (Bases de Datos Avanzadas), Universidad Autónoma de Chihuahua.
A web application that administers a blog whose data lives in Oracle. The application never executes SQL: every operation is a call to a stored procedure or function written in PL/SQL.
Code, comments and routes are in English. The user-facing text is in Spanish, since that is the language the application is presented in.
| Table | Purpose |
|---|---|
blog_users |
Blog authors (name, email) |
articles |
Posts (title, post_date, body_text CLOB) |
comments |
Reader replies attached to an article |
tags |
Tag catalog |
categories |
Category catalog |
article_tags |
Bridge table, article ↔ tag |
article_categories |
Bridge table, article ↔ category |
Relationships: users 1-to-N articles, users 1-to-N comments,
articles 1-to-N comments, articles N-to-N tags and N-to-N categories.
Organised by feature module in the NestJS style: each folder under app/modules/ owns everything about one entity and exposes a Flask blueprint.
app/
├── config.py Settings read from the environment
├── core/ Cross cutting pieces
│ ├── database.py Oracle connection, one per request
│ ├── oracle.py The only place that calls the PL/SQL packages
│ ├── exceptions.py AppError + ORA error translation
│ ├── request_parser.py Form field reading and validation (like pipes)
│ └── presentation.py Template filters and globals
├── modules/
│ ├── users/
│ │ ├── dto.py UserDto, UserSummaryDto, SaveUserDto
│ │ ├── repository.py Calls into blog_pkg / blog_web_pkg
│ │ ├── service.py Use cases
│ │ └── controller.py HTTP routes
│ ├── articles/ (+ taxonomy.py for the N-to-N links)
│ ├── comments/
│ ├── tags/
│ ├── categories/
│ └── dashboard/
├── templates/ Jinja2 + Bootstrap 5
└── static/css/app.css The few things Bootstrap does not cover
Every module follows the same chain:
controller -> service -> repository -> core/oracle -> blog_pkg
(HTTP) (use case) (one function (callproc /
per program) callfunc)
DTOs are frozen dataclasses with two entry points: from_row() builds one from
an Oracle result row, and from_form() builds one from the submitted form and
validates it. That keeps the validation next to the type it produces.
Bootstrap 5 with a custom theme in app/static/css/app.css.
The palette is sampled directly from the university crest (app/static/img/uach-crest.png) rather than picked by eye:
| Colour | Hex | Where it comes from | Where it is used |
|---|---|---|---|
| Purple | #8C2889 |
The ring | Accent colour and actions |
| Yellow | #F9ED25 |
The lightning bolt | Active icons, draft state |
| Green | #23945D |
The shield | Published state |
| Blue | #6EBDE3 |
The sky | Informational tints |
| Ink | #2D1F0F |
The wordmark | Text colour |
The look follows macOS: a translucent sidebar using backdrop-filter, a
sticky frosted toolbar, generous corner radii, hairline borders, soft layered
shadows, compact controls, and Inter standing in for SF Pro, which is not
licensed for the web.
The crest doubles as the sidebar logo and the favicon.
- Python 3.12 or newer
- Docker Desktop, for the Oracle XE container
- No Oracle Instant Client needed:
python-oracledbruns in thin mode - Bootstrap 5 and Bootstrap Icons load from a CDN, so there is no build step
docker start oracle-xe-hrIf the container does not exist yet:
docker run -d --name oracle-xe-hr -p 1521:1521 `
-e ORACLE_PASSWORD=oracle -e APP_USER=hr -e APP_USER_PASSWORD=hr `
gvenzl/oracle-xe:21-slimThe first run takes a couple of minutes. To confirm it is ready:
docker logs oracle-xe-hr --tail 5 # should print "DATABASE IS READY TO USE!"py -3.12 -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
copy .env.example .env.env ships with the container's credentials (hr / hr on
localhost:1521/xepdb1).
.venv\Scripts\python.exe -m scripts.setup_dbThis runs both .sql scripts, verifies that the packages compiled without
errors, and inserts 3 users, 5 tags, 3 categories, 3 articles and 4 comments.
The demo rows are created through the application's own services, so
seeding doubles as a check that the packages work.
Flags: --schema-only and --seed-only.
.venv\Scripts\python.exe main.pyOpen http://127.0.0.1:5000.
blog_pkg (in blog_database.sql) holds the entire
CRUD and is the only thing that writes to the tables.
blog_web_pkg (in blog_extras.sql) is read only. It
exists because the interface needs queries blog_pkg does not expose, above
all which tags and categories an article already carries: blog_pkg offers
assign_tag and remove_tag, but no way to read the relationship back.
Putting those SELECTs loose in Python would have broken the rule that the
application writes no SQL, so they live in a separate package instead.
blog_database.sql was never modified.
| Function | Returns | Module |
|---|---|---|
create_user |
user_id |
users |
create_article |
article_id |
articles |
create_comment |
comment_id |
comments |
create_tag |
tag_id |
tags |
create_category |
category_id |
categories |
count_article_comments |
a total | articles |
| Module | Procedures |
|---|---|
users |
get_user, list_users, update_user, delete_user |
articles |
get_article, list_articles, update_article, publish_article, delete_article |
comments |
get_comment, list_comments_by_article, update_comment, delete_comment |
tags |
get_tag, list_tags, update_tag, delete_tag |
categories |
get_category, list_categories, update_category, delete_category |
articles/taxonomy |
assign_tag, remove_tag, assign_category, remove_category |
dashboard, list_articles_full, get_article_full, list_article_tags,
list_article_categories, list_available_tags, list_available_categories,
list_tags_with_usage, list_categories_with_usage, list_users_with_usage.
-
Reference cursors. Query procedures return a
SYS_REFCURSOR. Python creates a cursor withconnection.cursor()and passes it as the last argument ofcallproc; on return it already points at the result set.core/oracle.fetch_allturns it into dictionaries keyed bycursor.description, and each module's DTO gives those rows a type. -
Transactions. The packages never
COMMIT. Python owns the transaction: a successful operation is committed, and anything Oracle raises is rolled back before the user is told about it. -
CLOB.
articles.body_textis aCLOB. Reading usesoracledb.defaults.fetch_lobs = Falseso the text arrives as a plainstr; writing needs nothing special, since python-oracledb binds astrto a CLOB parameter without the 32767 character ceiling aVARCHAR2bind would impose. -
Errors. The package's
RAISE_APPLICATION_ERRORcalls (ORA-20001 through ORA-20013) and constraint violations are translated bycore/exceptions.translateinto sentences a reader can act on, and theAppErrorhandler shows them as a Bootstrap alert on the same screen instead of a 500 page. -
Referential integrity. Deleting a user takes their articles with it (
ON DELETE CASCADE) and leaves their comments anonymous (ON DELETE SET NULL). Deleting an article takes its comments and links. The confirmation dialog spells out how many rows are about to be lost. -
Tags and categories share templates (
templates/catalog/) because they have the same columns and the same CRUD; each controller passes its own labels and endpoint names.