diff --git a/warehouse/__init__.py b/warehouse/__init__.py index 64379153013478f4f7cad240e8fbe06e79dd4b89..bbe5aec1978affa54cd639a5169a1aa566910f73 100644 --- a/warehouse/__init__.py +++ b/warehouse/__init__.py @@ -178,7 +178,37 @@ def item_delete(item_id): @app.route('/item/<item_id>/photo', methods=['POST']) def item_upload_photo(item_id): item = Item.query.get_or_404(item_id) - item.photos.append(Photo.from_form(request.files['file'])) + photo = Photo.from_form(request.files['file']) + item.photos.append(photo) + if not item.photo: + item.photo = photo + db.session.commit() + return redirect(url_for('item_view', item_id=item_id)) + +@app.route('/item/<item_id>/photo/<int:photo_id>/set', methods=['POST']) +def item_set_photo(item_id, photo_id): + item = Item.query.get_or_404(item_id) + photo = Photo.query.get_or_404(photo_id) + item.photo = photo + db.session.commit() + return redirect(url_for('item_view', item_id=item_id)) + +@app.route('/item/<item_id>/photo/clear', methods=['POST']) +def item_clear_photo(item_id): + item = Item.query.get_or_404(item_id) + item.photo = None + db.session.commit() + return redirect(url_for('item_view', item_id=item_id)) + +@app.route('/item/<item_id>/photo/<int:photo_id>/delete', methods=['GET', 'POST']) +def item_delete_photo(item_id, photo_id): + item = Item.query.get_or_404(item_id) + photo = Photo.query.get_or_404(photo_id) + if request.method == 'GET': + return render_template('item/delete_photo.html', item=item, photo=photo) + if item.photo == photo: + item.photo = None + item.photos.remove(photo) db.session.commit() return redirect(url_for('item_view', item_id=item_id)) diff --git a/warehouse/migrations/versions/bf26c4ca926e_primary_item_photo.py b/warehouse/migrations/versions/bf26c4ca926e_primary_item_photo.py new file mode 100644 index 0000000000000000000000000000000000000000..95c6e509e1943f0d7a0348b47f7d596102598637 --- /dev/null +++ b/warehouse/migrations/versions/bf26c4ca926e_primary_item_photo.py @@ -0,0 +1,46 @@ +"""Primary item photo + +Revision ID: bf26c4ca926e +Revises: aabfcc081e98 +Create Date: 2023-04-23 19:33:11.072575 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bf26c4ca926e' +down_revision = 'aabfcc081e98' +branch_labels = None +depends_on = None + +def upgrade(): + with op.batch_alter_table('item', schema=None) as batch_op: + batch_op.add_column(sa.Column('photo_id', sa.Integer(), nullable=True)) + batch_op.create_foreign_key('fk_item_photo_id_photo', 'photo', ['photo_id'], ['id']) + item_photo = sa.table( + 'item_photo', + sa.column('item_id', sa.String(5)), + sa.column('photo_id', sa.Integer()) + ) + item = sa.table( + 'item', + sa.column('id', sa.String(5)), + sa.column('photo_id', sa.Integer()) + ) + conn = op.get_bind() + conn.execute( + sa.update(item).values( + photo_id=sa.select([item_photo.c.photo_id]) + .where(item_photo.c.item_id == item.c.id) + .order_by(item_photo.c.photo_id) + .limit(1) + .scalar_subquery() + ) + ) + +def downgrade(): + with op.batch_alter_table('item', schema=None) as batch_op: + batch_op.drop_constraint('fk_item_photo_id_photo', type_='foreignkey') + batch_op.drop_column('photo_id') diff --git a/warehouse/models.py b/warehouse/models.py index e30bf8d3e32639dd06d1e2d59ef92259bbc52340..b81eed22213076b93d1ce68fcbc12375073f7213 100644 --- a/warehouse/models.py +++ b/warehouse/models.py @@ -82,6 +82,8 @@ class Item(db.Model): qr_code = db.Column(db.String(128), nullable=False, unique=True, default=token_qrfriendly) name = db.Column(db.String(128), nullable=False) description = db.Column(db.Text(), nullable=False, server_default='') + photo_id = db.Column(db.Integer(), db.ForeignKey('photo.id')) + photo = db.relationship('Photo') photos = db.relationship('Photo', secondary='item_photo') location_id = db.Column(db.String(5), db.ForeignKey('location.id')) location = db.relationship('Location') diff --git a/warehouse/templates/item/delete_photo.html b/warehouse/templates/item/delete_photo.html new file mode 100644 index 0000000000000000000000000000000000000000..87aa5d8f3988cd3dae0cdd7aa56ce03d0079e35b --- /dev/null +++ b/warehouse/templates/item/delete_photo.html @@ -0,0 +1,15 @@ +{% extends 'layout.html' %} + +{% block body %} +<form method="POST" class="form"> + <input type="hidden" name="csrf_token" value="{{ request.csrf_token }}"> + <p>Really delete this photo from item {{ item.name }} ({{ item.id }}):</p> + <div style="width: 390px; height: 300px;" class="d-flex align-items-center justify-content-center border text-bg-light"> + <img src="{{ url_for('photo_medium', photo_id=photo.id) }}" style="max-width: 100%; max-height: 100%;"> + </div> + <div class="d-flex justify-content-end gap-1"> + <a href="{{ url_for('item_view', item_id=item.id) }}" class="btn btn-light">Cancel</a> + <button type="submit" class="btn btn-danger">Delete photo</button> + </div> +</form> +{% endblock %} diff --git a/warehouse/templates/item/list.html b/warehouse/templates/item/list.html index 6c14261306bab17c85f7578e3c9ad8c93cf4bc90..7f83dda75a0d4d359e2a5751e767b90f4f15296a 100644 --- a/warehouse/templates/item/list.html +++ b/warehouse/templates/item/list.html @@ -22,9 +22,9 @@ {% for item in page.items %} <tr> <td style="width: 140px;"> - {% if item.photos %} + {% if item.photo %} <a href="{{ url_for('item_view', item_id=item.id) }}" style="width: 130px; height: 100px;" class="d-flex align-items-center justify-content-center border text-bg-light"> - <img src="{{ url_for('photo_small', photo_id=(item.photos|first).id) }}" style="max-width: 100%; max-height: 100%;"> + <img src="{{ url_for('photo_small', photo_id=item.photo.id) }}" style="max-width: 100%; max-height: 100%;"> </a> {% endif %} </td> diff --git a/warehouse/templates/item/view.html b/warehouse/templates/item/view.html index 88af4d6a1709a6ea83064298128e842109ba1875..5a81be6316fb018e94f65e41ce731323d709804c 100644 --- a/warehouse/templates/item/view.html +++ b/warehouse/templates/item/view.html @@ -37,34 +37,13 @@ <div class="row"> <div class="col-12 col-md-4"> - <div id="photo-carousel" class="carousel carousel-dark slide border text-bg-light p-1 my-2" style="width: 100%; height: 300px;"> - {% if item.photos|length > 1 %} - <div class="carousel-indicators"> - {% for photo in item.photos %} - <button type="button" data-bs-target="#photo-carousel" data-bs-slide-to="{{ loop.index0 }}" {% if loop.first %} class="active" aria-current="true" {% endif %}aria-label="Photo {{ loop.index }}"></button> - {% endfor %} - </div> - {% endif %} - <div class="carousel-inner" style="width: 100%; height: 100%;"> - {% for photo in item.photos %} - <div class="carousel-item {{ 'active' if loop.first }}" style="width: 100%; height: 100%;"> - <a href="{{ url_for('photo_show', photo_id=photo.id) }}" style="width: 100%; height: 100%;" class="d-flex align-items-center justify-content-center"> - <img src="{{ url_for('photo_medium', photo_id=photo.id) }}" style="max-width: 100%; max-height: 100%;"> - </a> - </div> - {% endfor %} - </div> - {% if item.photos|length > 1 %} - <button class="carousel-control-prev" type="button" data-bs-target="#photo-carousel" data-bs-slide="prev"> - <span class="carousel-control-prev-icon" aria-hidden="true"></span> - <span class="visually-hidden">Previous</span> - </button> - <button class="carousel-control-next" type="button" data-bs-target="#photo-carousel" data-bs-slide="next"> - <span class="carousel-control-next-icon" aria-hidden="true"></span> - <span class="visually-hidden">Next</span> - </button> - {% endif %} - </div> + {% if item.photo %} + <a href="{{ url_for('photo_show', photo_id=item.photo.id) }}" style="width: 100%; height: 100%;" class="d-flex align-items-center justify-content-center border text-bg-light p-1"> + <img src="{{ url_for('photo_medium', photo_id=item.photo.id) }}" style="max-width: 100%; max-height: 100%;"> + </a> + {% else %} + <div style="width: 100%; height: 100%; min-width: 390px; min-height: 300px;" class="d-flex align-items-center justify-content-center border text-bg-light"></div> + {% endif %} </div> <div class="col-12 col-md-8"> <h1>{{ item.name }}</h1> @@ -74,4 +53,37 @@ </div> </div> +<h2>Photos</h2> +<table class="table table-hover"> + <tbody> + {% for photo in item.photos %} + <tr> + <td style="width: 140px;"> + <a href="{{ url_for('photo_show', photo_id=photo.id) }}" style="width: 100%; height: 100%;" class="d-flex align-items-center justify-content-center border text-bg-light"> + <img src="{{ url_for('photo_small', photo_id=photo.id) }}" style="max-width: 100%; max-height: 100%;"> + </a> + </td> + <td> + <p>Uploaded {{ photo.created.date() }}</p> + </td> + <td> + <div class="d-flex justify-content-end gap-1"> + <a href="{{ url_for('item_delete_photo', item_id=item.id, photo_id=photo.id) }}" class="btn btn-danger">Delete</a> + {% if item.photo != photo %} + <form method="POST" action="{{ url_for('item_set_photo', item_id=item.id, photo_id=photo.id) }}"> + <input type="hidden" name="csrf_token" value="{{ request.csrf_token }}"> + <button type="submit" class="btn btn-primary">Make primary</button> + </form> + {% else %} + <form method="POST" action="{{ url_for('item_clear_photo', item_id=item.id) }}"> + <input type="hidden" name="csrf_token" value="{{ request.csrf_token }}"> + <button type="submit" class="btn btn-light">Clear primary</button> + </form> + {% endif %} + </div> + </td> + </tr> + {% endfor %} + </tbody> +</table> {% endblock %}