diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d86e312..616e6dd 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,15 +1,12 @@ -FROM mcr.microsoft.com/devcontainers/python:0-3.11 +FROM mcr.microsoft.com/devcontainers/python:3.12 ENV PYTHONUNBUFFERED 1 -# [Optional] If your requirements rarely change, uncomment this section to add them to the image. -# COPY requirements.txt /tmp/pip-tmp/ -# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ -# && rm -rf /tmp/pip-tmp - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - - +COPY requirements.txt /tmp/pip-tmp/ +RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ + && rm -rf /tmp/pip-tmp +# Installs the psql CLI. Note that to reach the DB from inside the devcontainer, +# we must run: psql -h localhost -U postgres (-d hackspace) +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends postgresql-client diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7642a44..7b73c7c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,24 +1,14 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/postgres { "name": "Python 3 & PostgreSQL", "dockerComposeFile": "docker-compose.yml", "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // This can be used to network with other containers or the host. - // "forwardPorts": [5000, 5432], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "pip install --user -r requirements.txt", - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "forwardPorts": [ 5432 ], + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ] + } + } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index f2e9705..b0d2c1c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,21 +5,13 @@ services: build: context: .. dockerfile: .devcontainer/Dockerfile - volumes: - ../..:/workspaces:cached - - # Overrides default command so things don't shut down after the process ends. command: sleep infinity - - # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. network_mode: service:db - # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) - db: - image: postgres:latest + image: postgres:15.6 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data @@ -28,8 +20,5 @@ services: POSTGRES_DB: postgres POSTGRES_PASSWORD: postgres - # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) - volumes: postgres-data: diff --git a/.gitignore b/.gitignore index 7260d15..945834e 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,5 @@ dmypy.json .pyre/ -*.csv \ No newline at end of file +*.csv +last_migrated.txt diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ca8de79 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Flask", + "type": "debugpy", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "hackspace_mgmt/__init__.py", + "FLASK_DEBUG": "1" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true, + "autoStartBrowser": true, + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index eb853eb..edd517a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -# hackspace-mgmt -Hackspace management portal +# Hackspace Portal + +Welcome! + +This repository contains the source code for the Hackspace Portal: a small website used to keep track of everything members are up to in the hackspace. + +Whether you're a seasoned hacker, a novice tinkerer, or simply curious about the world of making, this portal is your gateway to a vibrant community of like-minded individuals (nice intro ChatGPT). + +## Purpose of the Portal + +The portal serves as a point of administration for all members. It is commonly used for: + +- **Membership Administration**: Enrol members, manage personal details, forum accounts and references to payment information. +- **Access Cards**: Associate access cards with members and revoke them remotely. +- **Inductions**: Pass a test to gain access to some of the more scary machines, then unlock them with your card. +- **Storage Labels**: Track short term storage labels. + +## Getting Started + +See the `docs` folder for some guidance on setting up your dev environment and creating your first user. + +## Contributing + +We welcome contributions from all members of the community! Whether you're a seasoned developer or just getting started, there are plenty of ways to get involved. If you have ideas for new features, encounter any bugs, or simply want to help improve the platform, we'd love to hear from you. diff --git a/docs/01_dev_environment.md b/docs/01_dev_environment.md new file mode 100644 index 0000000..d386305 --- /dev/null +++ b/docs/01_dev_environment.md @@ -0,0 +1,46 @@ +# Dev Environment + +This is a somewhat straightforward python Flask app, backed by a Postgres database. + +### With the Dev Container + +The repository has a dev container configured which you can use if you wish. This greatly simplifies the development environment setup process as it installs a relevant version of python, the app's dependencies, postgres server and database, some CLI tools and even some extensions into your an isolated workspace. + +1. Install a dev container capable IDE (we're using vscode by default), Git and Docker/podman/your container orchestration tool of choice. +2. Ensure you have the Dev Containers extension installed. +3. Clone and open the repository. Vscode will nudge you to open it in a dev container. +4. Click "Run" in the debug pane. + +Both the dev container and postgres can be deleted and rebuilt as needed without losing your DB. On recreate, they will automatically re-attach themselves to the `hackspace-mgmt_devcontainer_postgres-data` volume. This contains the data itself, so it's the important one to keep safe. + +### Without the Dev Container + +
+ Expand + +#### Requiments: +- Python 3.9+ +- PostgreSQL 14+ +- Some ability to run Postgres queries directly - pgAdmin is a good GUI option, while `psql` is a good CLI. Both are bundled with Postgres. +- Git + +In a terminal, navigate to the `hackspace-mgmt` folder and create a virtual environment with `python3 -m venv .venv`. This environment can then be activated with `source .venv/bin/activate` or `.venv/Scripts/activate.ps1` depending on which OS/terminal you are using. + +Update pip with `python -m pip install --upgrade pip`. + +Install the requirements with `pip install -r requirements.txt`. + +You should now be able to run the server with `flask --app hackspace_mgmt:create_app --debug run` or by launching via the vscode debug pane. + +Navigate to `http://127.0.0.1:5000/admin/` and you should be able to see a bare admin page! + +
+ +### Database Setup + +The database schema is managed by the `Flask-Migrate` package, which will automatically create the database and update the schema for you on app startup. + +However, `./sample_dataset.sql` contains a pg_dump which can be useful for development environments as it contains a realistic set of data to test with. + +1. Create a fresh database using `psql -h localhost -U postgres -c "CREATE DATABASE hackspace"`. +2. Apply the dump using `psql -h localhost -U postgres hackspace < sample_dataset.sql`. diff --git a/docs/02_creating_your_first_user.md b/docs/02_creating_your_first_user.md new file mode 100644 index 0000000..ab7070c --- /dev/null +++ b/docs/02_creating_your_first_user.md @@ -0,0 +1,12 @@ +# Creating Your First User + +In order to use the member facing page of the server, we will need to create a test user we can login with. + +1. Create a test card. From the `/admin` page, navigate to _Membership->Card_, then click _Create_. Enter 0 as the serial for now. This is an integer, but everything else uses the hexadecimal representation. Enter 123 for the _Number On Front_, leave everything else as it is and click _Save_. +2. Create a test member. From the `/admin` page, navigate to _Membership->Member_, then click _Create_. Enter anything you want into the name and email fields. In the _Cards_ field, type the number 123 then select the entry that appears. Click _Save_ to finish. + +## Logging In + +You should now be able to navigate to `http://127.0.0.1:5000`. This will show the login page that's displayed on the member portal in the Hackspace. + +Behind the scenes, this page listens for keyboard input and will log you in when a valid card serial is "typed" in. To login with the test card, type _0 then Enter_. You will have to be quick as there is a 0.5s timeout after the page loads! \ No newline at end of file diff --git a/hackspace_mgmt/__init__.py b/hackspace_mgmt/__init__.py index e068fc8..4e59378 100644 --- a/hackspace_mgmt/__init__.py +++ b/hackspace_mgmt/__init__.py @@ -28,8 +28,10 @@ def create_app(test_config=None): scss = Bundle('scss/main.scss', filters='pyscss', depends=('scss/**/*.scss'), output='css/all.css') assets.register('css_all', scss) - from .models import db + from .models import db, migrate + db.init_app(app) + migrate.init_app(app, db) from . import general general.init_app(app) diff --git a/hackspace_mgmt/models.py b/hackspace_mgmt/models.py index ac8ee78..19ceeb9 100644 --- a/hackspace_mgmt/models.py +++ b/hackspace_mgmt/models.py @@ -8,9 +8,10 @@ from datetime import date from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate db = SQLAlchemy() - +migrate = Migrate() class InductionState(enum.Enum): valid = "valid" diff --git a/migration/00_initial.sql b/migration/00_initial.sql deleted file mode 100644 index c3bc313..0000000 --- a/migration/00_initial.sql +++ /dev/null @@ -1,327 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 13.10 (Debian 13.10-0+deb11u1) --- Dumped by pg_dump version 14.3 - --- Started on 2023-07-23 14:54:27 BST - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - - --- --- TOC entry 653 (class 1247 OID 16465) --- Name: discourse_invite; Type: TYPE; Schema: public; Owner: - --- - -CREATE TYPE public.discourse_invite AS ENUM ( - 'no', - 'invited', - 'expired', - 'accepted' -); - - --- --- TOC entry 632 (class 1247 OID 16388) --- Name: induction_state; Type: TYPE; Schema: public; Owner: - --- - -CREATE TYPE public.induction_state AS ENUM ( - 'valid', - 'expired', - 'banned' -); - - -SET default_table_access_method = heap; - --- --- TOC entry 203 (class 1259 OID 16414) --- Name: card; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.card ( - id integer NOT NULL, - card_serial bigint, - number_on_front integer, - member_id integer, - lost boolean DEFAULT false NOT NULL -); - - --- --- TOC entry 202 (class 1259 OID 16412) --- Name: card_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.card ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( - SEQUENCE NAME public.card_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- TOC entry 209 (class 1259 OID 16451) --- Name: induction; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.induction ( - id integer NOT NULL, - member_id integer NOT NULL, - machine_id integer NOT NULL, - state public.induction_state NOT NULL, - inducted_by integer, - inducted_on date DEFAULT now() -); - - --- --- TOC entry 208 (class 1259 OID 16449) --- Name: induction_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.induction ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( - SEQUENCE NAME public.induction_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- TOC entry 205 (class 1259 OID 16425) --- Name: machine; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.machine ( - id integer NOT NULL, - name character varying(255) NOT NULL -); - - --- --- TOC entry 207 (class 1259 OID 16437) --- Name: machine_controller; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.machine_controller ( - id integer NOT NULL, - mac bigint, - machine_id integer, - requires_update boolean NOT NULL -); - - --- --- TOC entry 206 (class 1259 OID 16435) --- Name: machine_controller_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.machine_controller ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( - SEQUENCE NAME public.machine_controller_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- TOC entry 204 (class 1259 OID 16423) --- Name: machine_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.machine ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( - SEQUENCE NAME public.machine_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- TOC entry 200 (class 1259 OID 16395) --- Name: member; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.member ( - id integer NOT NULL, - first_name character varying(80) NOT NULL, - last_name character varying(80), - discourse_id integer, - discourse public.discourse_invite DEFAULT 'no'::public.discourse_invite NOT NULL, - mailchimp boolean DEFAULT false NOT NULL, - email character varying(300), - alt_email character varying(300), - payment_ref character varying(200), - join_date date DEFAULT CURRENT_DATE NOT NULL, - end_date date, - end_reason character varying(500), - address1 character varying(200), - address2 character varying(200), - town_city character varying(200), - county character varying(200), - postcode character varying(20), - payment_active boolean DEFAULT false NOT NULL, - notes text -); - - --- --- TOC entry 201 (class 1259 OID 16406) --- Name: member_data_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.member ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( - SEQUENCE NAME public.member_data_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- TOC entry 2893 (class 2606 OID 16420) --- Name: card card_card_serial_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.card - ADD CONSTRAINT card_card_serial_key UNIQUE (card_serial); - - --- --- TOC entry 2895 (class 2606 OID 16422) --- Name: card card_number_on_front_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.card - ADD CONSTRAINT card_number_on_front_key UNIQUE (number_on_front); - - --- --- TOC entry 2897 (class 2606 OID 16418) --- Name: card card_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.card - ADD CONSTRAINT card_pkey PRIMARY KEY (id); - - --- --- TOC entry 2905 (class 2606 OID 16455) --- Name: induction induction_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.induction - ADD CONSTRAINT induction_pkey PRIMARY KEY (id); - - --- --- TOC entry 2901 (class 2606 OID 16443) --- Name: machine_controller machine_controller_mac_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.machine_controller - ADD CONSTRAINT machine_controller_mac_key UNIQUE (mac); - - --- --- TOC entry 2903 (class 2606 OID 16441) --- Name: machine_controller machine_controller_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.machine_controller - ADD CONSTRAINT machine_controller_pkey PRIMARY KEY (id); - - --- --- TOC entry 2899 (class 2606 OID 16429) --- Name: machine machine_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.machine - ADD CONSTRAINT machine_pkey PRIMARY KEY (id); - - --- --- TOC entry 2891 (class 2606 OID 16399) --- Name: member member_data_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.member - ADD CONSTRAINT member_data_pkey PRIMARY KEY (id); - - --- --- TOC entry 2906 (class 2606 OID 24654) --- Name: card card_member_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.card - ADD CONSTRAINT card_member_id_fkey FOREIGN KEY (member_id) REFERENCES public.member(id) NOT VALID; - - --- --- TOC entry 2910 (class 2606 OID 24664) --- Name: induction induction_inducted_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.induction - ADD CONSTRAINT induction_inducted_by_fkey FOREIGN KEY (inducted_by) REFERENCES public.member(id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; - - --- --- TOC entry 2908 (class 2606 OID 16456) --- Name: induction induction_machine_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.induction - ADD CONSTRAINT induction_machine_id_fkey FOREIGN KEY (machine_id) REFERENCES public.machine(id) ON UPDATE RESTRICT ON DELETE RESTRICT; - - --- --- TOC entry 2909 (class 2606 OID 24659) --- Name: induction induction_member_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.induction - ADD CONSTRAINT induction_member_id_fkey FOREIGN KEY (member_id) REFERENCES public.member(id) ON UPDATE RESTRICT ON DELETE RESTRICT NOT VALID; - - --- --- TOC entry 2907 (class 2606 OID 16444) --- Name: machine_controller machine_controller_machine_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.machine_controller - ADD CONSTRAINT machine_controller_machine_id_fkey FOREIGN KEY (machine_id) REFERENCES public.machine(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- Completed on 2023-07-23 14:54:28 BST - --- --- PostgreSQL database dump complete --- - diff --git a/migration/01_preferred_name.sql b/migration/01_preferred_name.sql deleted file mode 100644 index ebffaa3..0000000 --- a/migration/01_preferred_name.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IF EXISTS public.member - ADD COLUMN preferred_first_name character varying(80); - -ALTER TABLE IF EXISTS public.member - ADD COLUMN preferred_last_name character varying(80); \ No newline at end of file diff --git a/migration/02_preferred_name.sql b/migration/02_preferred_name.sql deleted file mode 100644 index e93b976..0000000 --- a/migration/02_preferred_name.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE IF EXISTS public.member DROP COLUMN IF EXISTS preferred_last_name; - -ALTER TABLE IF EXISTS public.member - RENAME preferred_first_name TO preferred_name; - -ALTER TABLE public.member - ALTER COLUMN preferred_name TYPE character varying(160) COLLATE pg_catalog."default"; \ No newline at end of file diff --git a/migration/03_newsletter.sql b/migration/03_newsletter.sql deleted file mode 100644 index 405410b..0000000 --- a/migration/03_newsletter.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.member - RENAME mailchimp TO newsletter; \ No newline at end of file diff --git a/migration/04_machine_power.sql b/migration/04_machine_power.sql deleted file mode 100644 index ee9331c..0000000 --- a/migration/04_machine_power.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.machine_controller - ADD COLUMN powered boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migration/05_label.sql b/migration/05_label.sql deleted file mode 100644 index 558eacc..0000000 --- a/migration/05_label.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE public.label -( - id integer NOT NULL GENERATED ALWAYS AS IDENTITY, - member_id integer, - expiry date NOT NULL, - caption character varying(255) NOT NULL, - printed boolean NOT NULL DEFAULT false, - PRIMARY KEY (id), - FOREIGN KEY (member_id) - REFERENCES public.member (id) MATCH SIMPLE - ON UPDATE CASCADE - ON DELETE SET NULL - NOT VALID -); \ No newline at end of file diff --git a/migration/06_controller_settings.sql b/migration/06_controller_settings.sql deleted file mode 100644 index 7032ec6..0000000 --- a/migration/06_controller_settings.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE IF EXISTS public.machine_controller - ADD COLUMN idle_timeout integer NOT NULL DEFAULT -1; - -ALTER TABLE IF EXISTS public.machine_controller - ADD COLUMN idle_power_threshold integer NOT NULL DEFAULT 50; - -ALTER TABLE IF EXISTS public.machine_controller - ADD COLUMN invert_logout_button boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migration/07_discourse_emailed.sql b/migration/07_discourse_emailed.sql deleted file mode 100644 index defaa65..0000000 --- a/migration/07_discourse_emailed.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TYPE public.discourse_invite RENAME VALUE 'expired' TO 'emailed' \ No newline at end of file diff --git a/migration/08_welcome_email.sql b/migration/08_welcome_email.sql deleted file mode 100644 index 65d75a7..0000000 --- a/migration/08_welcome_email.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IF EXISTS public.member - ADD COLUMN welcome_email_sent boolean NOT NULL DEFAULT true; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN welcome_email_sent SET DEFAULT false; \ No newline at end of file diff --git a/migration/09_card_verified.sql b/migration/09_card_verified.sql deleted file mode 100644 index 8482499..0000000 --- a/migration/09_card_verified.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.card - ADD COLUMN unverified_serial bigint; \ No newline at end of file diff --git a/migration/10_quiz.sql b/migration/10_quiz.sql deleted file mode 100644 index 97facff..0000000 --- a/migration/10_quiz.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE public.quiz -( - id integer NOT NULL GENERATED ALWAYS AS IDENTITY, - title character varying(200) NOT NULL, - description text, - questions text NOT NULL, - machine_id integer, - PRIMARY KEY (id), - FOREIGN KEY (machine_id) - REFERENCES public.machine (id) MATCH SIMPLE - ON UPDATE CASCADE - ON DELETE SET NULL - NOT VALID -); \ No newline at end of file diff --git a/migration/11_unique_induction.sql b/migration/11_unique_induction.sql deleted file mode 100644 index d0211c8..0000000 --- a/migration/11_unique_induction.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.induction - ADD UNIQUE (member_id, machine_id); \ No newline at end of file diff --git a/migration/12_alumni.sql b/migration/12_alumni.sql deleted file mode 100644 index 8152667..0000000 --- a/migration/12_alumni.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TYPE public.discourse_invite - ADD VALUE 'alumni' AFTER 'accepted'; \ No newline at end of file diff --git a/migration/13_card_disabled.sql b/migration/13_card_disabled.sql deleted file mode 100644 index bc238f3..0000000 --- a/migration/13_card_disabled.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.card - ADD COLUMN door_disabled boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migration/14_quiz_intro.sql b/migration/14_quiz_intro.sql deleted file mode 100644 index b6154dc..0000000 --- a/migration/14_quiz_intro.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IF EXISTS public.quiz - ADD COLUMN intro text NOT NULL DEFAULT ''; - -ALTER TABLE IF EXISTS public.quiz - ADD COLUMN hidden boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migration/15_machine_hostname.sql b/migration/15_machine_hostname.sql deleted file mode 100644 index 22fc89d..0000000 --- a/migration/15_machine_hostname.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE IF EXISTS public.machine_controller - ADD COLUMN hostname character varying(255) NOT NULL DEFAULT ''; -UPDATE public.machine_controller SET hostname = mac WHERE hostname = ''; -ALTER TABLE IF EXISTS public.machine_controller - ADD UNIQUE (hostname); -ALTER TABLE IF EXISTS public.machine_controller DROP COLUMN IF EXISTS mac; \ No newline at end of file diff --git a/migration/16_machine_legacy.sql b/migration/16_machine_legacy.sql deleted file mode 100644 index aed46a4..0000000 --- a/migration/16_machine_legacy.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TYPE public.legacy_machine_auth AS ENUM ( - 'none', - 'password', - 'padlock' -); - -ALTER TABLE IF EXISTS public.machine - ADD COLUMN legacy_auth public.legacy_machine_auth NOT NULL DEFAULT 'none'::public.legacy_machine_auth; - -ALTER TABLE IF EXISTS public.machine - ADD COLUMN legacy_password character varying(255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/migration/17_hidden_machine.sql b/migration/17_hidden_machine.sql deleted file mode 100644 index f830176..0000000 --- a/migration/17_hidden_machine.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.machine ADD COLUMN hide_from_home boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migration/18_can_induct.sql b/migration/18_can_induct.sql deleted file mode 100644 index b103e14..0000000 --- a/migration/18_can_induct.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IF EXISTS public.induction - ADD COLUMN can_induct boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migration/19_address_not_null.sql b/migration/19_address_not_null.sql deleted file mode 100644 index 2cdc656..0000000 --- a/migration/19_address_not_null.sql +++ /dev/null @@ -1,39 +0,0 @@ -UPDATE public.member SET address1 = '' where address1 IS NULL; - -UPDATE public.member SET address2 = '' where address2 IS NULL; - -UPDATE public.member SET town_city = '' where town_city IS NULL; - -UPDATE public.member SET county = '' where county IS NULL; - -UPDATE public.member SET postcode = '' where postcode IS NULL; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN address1 SET DEFAULT ''; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN address1 SET NOT NULL; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN address2 SET DEFAULT ''; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN address2 SET NOT NULL; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN town_city SET DEFAULT ''; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN town_city SET NOT NULL; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN county SET DEFAULT ''; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN county SET NOT NULL; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN postcode SET DEFAULT ''; - -ALTER TABLE IF EXISTS public.member - ALTER COLUMN postcode SET NOT NULL; \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/0f17df0ee1a8_.py b/migrations/versions/0f17df0ee1a8_.py new file mode 100644 index 0000000..e1e1526 --- /dev/null +++ b/migrations/versions/0f17df0ee1a8_.py @@ -0,0 +1,242 @@ +"""empty message + +Revision ID: 0f17df0ee1a8 +Revises: +Create Date: 2024-05-01 20:28:38.516679 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '0f17df0ee1a8' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('card', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.alter_column('card_serial', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=True) + batch_op.alter_column('unverified_serial', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=True) + batch_op.create_unique_constraint(None, ['unverified_serial']) + + with op.batch_alter_table('induction', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.alter_column('inducted_on', + existing_type=sa.DATE(), + nullable=False, + existing_server_default=sa.text('now()')) + batch_op.drop_constraint('induction_machine_id_fkey', type_='foreignkey') + batch_op.drop_constraint('induction_inducted_by_fkey', type_='foreignkey') + batch_op.drop_constraint('induction_member_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'member', ['member_id'], ['id']) + batch_op.create_foreign_key(None, 'member', ['inducted_by'], ['id']) + batch_op.create_foreign_key(None, 'machine', ['machine_id'], ['id']) + + with op.batch_alter_table('label', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.drop_constraint('label_member_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'member', ['member_id'], ['id']) + + with op.batch_alter_table('machine', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.alter_column('name', + existing_type=sa.VARCHAR(length=255), + type_=sa.String(length=100), + existing_nullable=False) + batch_op.alter_column('legacy_auth', + existing_type=postgresql.ENUM('none', 'password', 'padlock', name='legacy_machine_auth'), + type_=sa.Enum('none', 'password', 'padlock', name='legacy_auth'), + existing_nullable=False, + existing_server_default=sa.text("'none'::legacy_machine_auth")) + + with op.batch_alter_table('machine_controller', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.alter_column('machine_id', + existing_type=sa.INTEGER(), + nullable=False) + batch_op.drop_constraint('machine_controller_machine_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'machine', ['machine_id'], ['id']) + + with op.batch_alter_table('member', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.alter_column('notes', + existing_type=sa.TEXT(), + type_=sa.String(), + existing_nullable=True) + batch_op.create_unique_constraint(None, ['discourse_id']) + + with op.batch_alter_table('quiz', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=None, + existing_nullable=False, + autoincrement=True) + batch_op.alter_column('title', + existing_type=sa.VARCHAR(length=200), + type_=sa.String(length=255), + existing_nullable=False) + batch_op.alter_column('description', + existing_type=sa.TEXT(), + type_=sa.String(), + existing_nullable=True) + batch_op.alter_column('questions', + existing_type=sa.TEXT(), + type_=sa.String(), + existing_nullable=False) + batch_op.alter_column('intro', + existing_type=sa.TEXT(), + type_=sa.String(), + existing_nullable=False, + existing_server_default=sa.text("''::text")) + batch_op.drop_constraint('quiz_machine_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'machine', ['machine_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('quiz', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('quiz_machine_id_fkey', 'machine', ['machine_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + batch_op.alter_column('intro', + existing_type=sa.String(), + type_=sa.TEXT(), + existing_nullable=False, + existing_server_default=sa.text("''::text")) + batch_op.alter_column('questions', + existing_type=sa.String(), + type_=sa.TEXT(), + existing_nullable=False) + batch_op.alter_column('description', + existing_type=sa.String(), + type_=sa.TEXT(), + existing_nullable=True) + batch_op.alter_column('title', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=200), + existing_nullable=False) + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + with op.batch_alter_table('member', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + batch_op.alter_column('notes', + existing_type=sa.String(), + type_=sa.TEXT(), + existing_nullable=True) + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + with op.batch_alter_table('machine_controller', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('machine_controller_machine_id_fkey', 'machine', ['machine_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + batch_op.alter_column('machine_id', + existing_type=sa.INTEGER(), + nullable=True) + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + with op.batch_alter_table('machine', schema=None) as batch_op: + batch_op.alter_column('legacy_auth', + existing_type=sa.Enum('none', 'password', 'padlock', name='legacy_auth'), + type_=postgresql.ENUM('none', 'password', 'padlock', name='legacy_machine_auth'), + existing_nullable=False, + existing_server_default=sa.text("'none'::legacy_machine_auth")) + batch_op.alter_column('name', + existing_type=sa.String(length=100), + type_=sa.VARCHAR(length=255), + existing_nullable=False) + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + with op.batch_alter_table('label', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('label_member_id_fkey', 'member', ['member_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + with op.batch_alter_table('induction', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('induction_member_id_fkey', 'member', ['member_id'], ['id'], onupdate='RESTRICT', ondelete='RESTRICT') + batch_op.create_foreign_key('induction_inducted_by_fkey', 'member', ['inducted_by'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + batch_op.create_foreign_key('induction_machine_id_fkey', 'machine', ['machine_id'], ['id'], onupdate='RESTRICT', ondelete='RESTRICT') + batch_op.alter_column('inducted_on', + existing_type=sa.DATE(), + nullable=True, + existing_server_default=sa.text('now()')) + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + with op.batch_alter_table('card', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + batch_op.alter_column('unverified_serial', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=True) + batch_op.alter_column('card_serial', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=True) + batch_op.alter_column('id', + existing_type=sa.INTEGER(), + server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1), + existing_nullable=False, + autoincrement=True) + + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 2232d97..cbb5518 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ flask>=2.0 yarl requests +Flask-Migrate Flask-SQLAlchemy psycopg2-binary>=2.9.0 Flask-Admin diff --git a/sample_dataset.sql b/sample_dataset.sql index 517711b..f579f90 100644 --- a/sample_dataset.sql +++ b/sample_dataset.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 16.2 --- Dumped by pg_dump version 16.2 +-- Dumped from database version 15.6 (Debian 15.6-1.pgdg120+2) +-- Dumped by pg_dump version 15.6 (Debian 15.6-0+deb12u1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -61,6 +61,17 @@ SET default_tablespace = ''; SET default_table_access_method = heap; +-- +-- Name: alembic_version; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.alembic_version ( + version_num character varying(32) NOT NULL +); + + +ALTER TABLE public.alembic_version OWNER TO postgres; + -- -- Name: card; Type: TABLE; Schema: public; Owner: postgres -- @@ -289,6 +300,15 @@ ALTER TABLE public.quiz ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( ); +-- +-- Data for Name: alembic_version; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.alembic_version (version_num) FROM stdin; +0f17df0ee1a8 +\. + + -- -- Data for Name: card; Type: TABLE DATA; Schema: public; Owner: postgres -- @@ -412,6 +432,14 @@ SELECT pg_catalog.setval('public.member_data_id_seq', 7, true); SELECT pg_catalog.setval('public.quiz_id_seq', 1, true); +-- +-- Name: alembic_version alembic_version_pkc; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.alembic_version + ADD CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num); + + -- -- Name: card card_card_serial_key; Type: CONSTRAINT; Schema: public; Owner: postgres --