~muirrum/lost

e9e97daedb98af7af00bf1fbc73e488180f4225d — Cara Salter 4 months ago ea09869
Enable asset import from csv
M lost/__init__.py => lost/__init__.py +14 -0
@@ 2,10 2,12 @@ from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_assets import Environment, Bundle

db = SQLAlchemy()
login = LoginManager()
migrate = Migrate()
env = Environment()

def create_app():
    app = Flask(__name__)


@@ 16,10 18,22 @@ def create_app():
    migrate.init_app(app, db)
    login.init_app(app)

    # init assets
    env.init_app(app)
    scss = Bundle('scss/style.scss', filters='scss', output='gen/style.css')
    env.register('scss', scss)

    from .models import User

    from lost import cli

    from lost.auth import bp as auth_bp
    app.register_blueprint(auth_bp)

    from lost.meta import bp as meta_bp
    app.register_blueprint(meta_bp)

    app.cli.add_command(cli.gr)
    app.cli.add_command(cli.assets)

    return app

M lost/auth/__init__.py => lost/auth/__init__.py +1 -0
@@ 5,6 5,7 @@ from werkzeug.security import check_password_hash
from datetime import datetime

from lost.models import User
from lost import db
from .forms import LoginForm

import ulid

M lost/cli.py => lost/cli.py +28 -2
@@ 1,16 1,21 @@
import click
from flask import current_app

from flask.cli import AppGroup
from werkzeug.security import generate_password_hash
from datetime import datetime

from lost.models import User
from lost.models import User, Asset
from ulid import ULID

from lost import db

import csv

gr = AppGroup("user")

assets = AppGroup("asset")

@gr.command("create")
@click.option("--email", prompt=True)
@click.option("--pref_name", prompt=True)


@@ 33,4 38,25 @@ def create_user(email, pref_name, password):
    db.session.add(user)
    db.session.commit()

    click.echo("Created user")
\ No newline at end of file
    click.echo("Created user")

@assets.command("import")
@click.option("--file", prompt=True)
@click.option("--delimeter", default=",")
def import_assets(file, delimeter):
    with open(file) as csv_file:
#        current_app.debug(f"Attempting to import assets from {}")
        csv_reader = csv.reader(csv_file, delimiter=delimeter)
        line_count = 0
        for row in csv_reader:
            line_count += 1
            click.echo(f"{row[0]} has number {row[1]}")
            asset = Asset(
                id=str(ULID()),
                name=row[0],
                asset_number=row[1]
            )
            db.session.add(asset)
        db.session.commit()

        click.echo(f"Successfully imported {line_count} assets")
\ No newline at end of file

A lost/meta/__init__.py => lost/meta/__init__.py +12 -0
@@ 0,0 1,12 @@
from flask import Blueprint, render_template
from flask_login import login_required

from lost.models import Asset

bp = Blueprint('meta', __name__, url_prefix='/meta')

@bp.route('/dashboard')
@login_required
def dashboard():
    assets = Asset.query.all()
    return render_template('assets.html', assets=assets)
\ No newline at end of file

M lost/models.py => lost/models.py +6 -0
@@ 17,6 17,12 @@ class User(db.Model, UserMixin):
    def __str__(self):
        return self.pref_name
    
class Asset(db.Model):
    id = Column(String, primary_key=True)
    asset_number = Column(Integer, unique=True, autoincrement=True, nullable=False, index=True)
    name = Column(String, nullable=False)
    last_found = Column(DateTime, nullable=True)

@login.user_loader
def load_user(user_id):
    return User.query.filter_by(id=user_id).one_or_none()

A lost/static/gen/style.css => lost/static/gen/style.css +110 -0
@@ 0,0 1,110 @@
body {
  background: #282828;
  color: #ebdbb2;
  font-family: monospace;
}

a a:active, a:visited {
  color: #458588;
}

.container {
  margin: auto;
  width: 60%;
}

button,
input[type=submit] {
  border-radius: 8px;
  background-color: #458588;
  border-color: #458588;
  border: none;
  margin: 0.5rem;
}

button.accent {
  background-color: #d79921;
  border-color: #d79921;
}

h1, h2, h3, h4, h5, h6 {
  border-bottom: 1px solid;
  width: 50%;
}

.navbar {
  list-style-type: none;
  margin: 0;
  padding: 0;
  border-bottom: 1px solid;
  margin-bottom: 2rem;
  padding-bottom: 0.4rem;
  text-align: center;
}

.navbar-item {
  display: inline;
  margin-right: 1rem;
}

.flashes {
  list-style-type: none;
  display: flex;
  justify-content: center;
}

.message {
  width: 80%;
  justify-content: center;
  border: 1px solid #ebdbb2;
  background-color: #d79921;
  padding: 0.2rem;
  font-size: large;
  color: black;
}

form {
  width: 40%;
}

label,
input {
  margin-bottom: 0.5rem;
  margin-top: 0.5rem;
  display: inline-block;
}

label {
  width: 40%;
  text-align: left;
}

label + input {
  width: 40%;
  margin: 0 30% 0 4%;
}

table {
  border-collapse: collapse;
  border-spacing: 10px;
  width: 50%;
}

table td {
  padding-top: 0.5rem;
  padding-left: 1.5rem;
}

td, th {
  border-left: 1px solid #ebdbb2;
  border-bottom: 1px solid #ebdbb2;
  width: 1.5rem;
}

tr:last-child td {
  border-bottom: none;
}

td:first-child, th:first-child {
  border-left: none;
}

A lost/static/scss/style.scss => lost/static/scss/style.scss +123 -0
@@ 0,0 1,123 @@
$color-bg: #282828;
$color-shadow: #3c3836;
$color-fg: #ebdbb2;
$color-accent-bg: #d79921;
$color-accent: #504945;
$color-link: #458588;
$color-danger: #cc241d;
$font-family: monospace;

body {
    background: $color-bg;
    color: $color-fg;
    font-family: $font-family;
}

a a:active, a:visited {
    color: $color-link;
}

.container {
    margin: auto;
    width: 60%;
}

button,
input[type=submit]{
    border-radius: 8px;
    background-color: $color-link;
    border-color: $color-link;
    border: none;
    margin: 0.5rem;
}
    
button.accent {
    background-color: $color-accent-bg;
    border-color: $color-accent-bg;
}

h1,h2,h3,h4,h5,h6 {
    border-bottom: 1px solid;
    width:50%;
}

// Navbar

.navbar {
    list-style-type: none;
    margin: 0;
    padding: 0;
    border-bottom: 1px solid;
    margin-bottom: 2rem;
    padding-bottom: 0.4rem;
    text-align: center;
}
.navbar-item {
    display: inline;
    margin-right: 1rem;
}

// Flashed messages
.flashes {
    list-style-type: none;
    display: flex;
    justify-content: center;
}

.message {
    width: 80%;
    justify-content: center; 
    border: 1px solid $color-fg;
    background-color: $color-accent-bg;
    padding: 0.2rem;
    font-size: large;
    color: black;
}

// Forms
form {
    width: 40%;
}

label,
input {
    margin-bottom: 0.5rem;
    margin-top: 0.5rem;
    display: inline-block;
}

label {
    width: 40%;
    text-align: left;
}

label+input {
    width: 40%; 
    margin: 0 30% 0 4%;
}

// Tables
table {
    border-collapse: collapse;
    border-spacing: 10px;
    width: 50%;
    td {
        padding-top: 0.5rem;
        padding-left: 1.5rem;
    }
}

td, th {
    border-left: 1px solid $color-fg;
    border-bottom: 1px solid $color-fg;
    width: 1.5rem;
}
tr:last-child {
    td {
    border-bottom: none;
    }
}

td:first-child, th:first-child {
    border-left: none;
}

A lost/templates/assets.html => lost/templates/assets.html +28 -0
@@ 0,0 1,28 @@
{% extends 'layout.html' %}

{% block content %}
<h1>Assets</h1>

<table>
  <thead>
    <tr>
      <th scope="col">ID</th>
      <th scope="col">Asset Number</th>
      <th scope="col">Name</th>
      <th scope="col">Last Found</th>
      <th scope="col">Delete</th>
    </tr>
  </thead>
  <tbody>
    {% for n in assets %}
    <tr>
      <td>{{n.id}}</td>
      <td>{{n.asset_number}}</td>
      <td>{{n.name}}</td>
      <td>{{n.last_found}}</td>
      <td><a href="#">Delete</a></td>
    </tr>
    {% endfor %}
  </tbody>
</table>
{% endblock %}
\ No newline at end of file

A lost/templates/layout.html => lost/templates/layout.html +31 -0
@@ 0,0 1,31 @@
<!DOCTYPE html>

<html>
    <head>
        <title>Lost</title>
        {% assets "scss" %}
        <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
        {% endassets %}
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>

    <body>
        <div class="container">
            {% with messages = get_flashed_messages() %}
                {% if messages %}
                <ul class="flashes">
                    {% for m in messages %}
                    <li class="message">{{m}}</li>
                    {% endfor %}
                </ul>
                {% endif %}
            {% endwith %}

            {% block content %}

            Whoops! This page is still being worked on.

            {% endblock %}
        </div>
    </body>
</html>
\ No newline at end of file

A lost/templates/login.html => lost/templates/login.html +14 -0
@@ 0,0 1,14 @@
{% extends 'layout.html' %}

{% block content %}
<form method="POST">
    {{ form.csrf_token }}
    <div>
    {{form.username.label }}{{form.username}}
    </div>
    <div>
    {{form.password.label}}{{form.password}}
    </div>
    {{form.submit}}
</form>
{% endblock %}
\ No newline at end of file

A migrations/versions/8865e24d9b38_create_asset.py => migrations/versions/8865e24d9b38_create_asset.py +40 -0
@@ 0,0 1,40 @@
"""Create asset

Revision ID: 8865e24d9b38
Revises: 821e018f114a
Create Date: 2024-01-22 16:39:47.315735

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '8865e24d9b38'
down_revision = '821e018f114a'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('asset',
    sa.Column('id', sa.String(), nullable=False),
    sa.Column('asset_number', sa.Integer(), autoincrement=True, nullable=False),
    sa.Column('name', sa.String(), nullable=False),
    sa.Column('last_found', sa.DateTime(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    with op.batch_alter_table('asset', schema=None) as batch_op:
        batch_op.create_index(batch_op.f('ix_asset_asset_number'), ['asset_number'], unique=True)

    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('asset', schema=None) as batch_op:
        batch_op.drop_index(batch_op.f('ix_asset_asset_number'))

    op.drop_table('asset')
    # ### end Alembic commands ###

A test.csv => test.csv +1 -0
@@ 0,0 1,1 @@
Framework,0001