1) added dynamic SAR calls list reloading

2) added support for uploading images and files
3) refactored sar_details
This commit is contained in:
Vadim Likholetov 2023-12-09 23:21:44 +02:00
parent a9b074a42b
commit a33c77d2d0
16 changed files with 673 additions and 374 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/storage/
/instance/
/migrations/
/certs/
/.idea/

View File

@ -5,6 +5,8 @@
</component> </component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
<excludeFolder url="file://$MODULE_DIR$/storage" />
<excludeFolder url="file://$MODULE_DIR$/venv" /> <excludeFolder url="file://$MODULE_DIR$/venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.9 (SARBase)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.9 (SARBase)" jdkType="Python SDK" />

View File

@ -2,7 +2,7 @@ from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView from flask_admin.contrib.sqla import ModelView
from flask_login import current_user from flask_login import current_user
from app import app, db from app import app, db
from models import User, Role, SARCall, Comment, SARCategory, GPSTrack, SARStatus, SARResult from models import User, Role, SARCall, Comment, SARCategory, GPSTrack, SARStatus, SARResult, FileAttachment
class AdminModelView(ModelView): class AdminModelView(ModelView):
def is_accessible(self): def is_accessible(self):
@ -26,6 +26,7 @@ admin.add_view(UserModelView(User, db.session))
admin.add_view(AdminModelView(SARCall, db.session)) admin.add_view(AdminModelView(SARCall, db.session))
admin.add_view(AdminModelView(Comment, db.session)) admin.add_view(AdminModelView(Comment, db.session))
admin.add_view(AdminModelView(GPSTrack, db.session)) admin.add_view(AdminModelView(GPSTrack, db.session))
admin.add_view(AdminModelView(FileAttachment, db.session))
admin.add_view(AdminModelView(Role, db.session, category="Dictionaries")) admin.add_view(AdminModelView(Role, db.session, category="Dictionaries"))
admin.add_view(AdminModelView(SARCategory, db.session, category="Dictionaries")) admin.add_view(AdminModelView(SARCategory, db.session, category="Dictionaries"))
admin.add_view(AdminModelView(SARStatus, db.session, category="Dictionaries")) admin.add_view(AdminModelView(SARStatus, db.session, category="Dictionaries"))

9
app.py
View File

@ -28,6 +28,13 @@ app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key' app.config['SECRET_KEY'] = 'secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'mysql+mysqlconnector://sarbaseuser:password@localhost/sarbaseapp') app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'mysql+mysqlconnector://sarbaseuser:password@localhost/sarbaseapp')
app.config['BABEL_DEFAULT_LOCALE'] = 'en' app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['MAX_CONTENT_LENGTH'] = 128 * 1024 * 1024
if os.environ.get('DOCKER_ENV') == 'true':
app.config['STORAGE_DIR'] = '/storage'
else:
app.config['STORAGE_DIR'] = './storage'
#app.debug = True #app.debug = True
#toolbar = DebugToolbarExtension(app) #toolbar = DebugToolbarExtension(app)
babel= Babel(app) babel= Babel(app)
@ -37,11 +44,11 @@ migrate = Migrate(app, db)
login_manager = LoginManager(app) login_manager = LoginManager(app)
import models import models
import admin import admin
import login import login
import sar_calls import sar_calls
import sar_call_details
import dashboard import dashboard

View File

@ -10,8 +10,10 @@ services:
environment: environment:
- FLASK_ENV=development - FLASK_ENV=development
- DATABASE_URL=mysql+mysqlconnector://sarbaseuser:password@db/sarbaseapp - DATABASE_URL=mysql+mysqlconnector://sarbaseuser:password@db/sarbaseapp
- DOCKER_ENV=true
volumes: volumes:
- ./certs:/certs - ./certs:/certs
- ./storage:/storage
nginx: nginx:
build: build:

File diff suppressed because one or more lines are too long

View File

@ -104,3 +104,14 @@ class Comment(db.Model):
user = db.relationship('User', backref=db.backref('comments', lazy=True)) user = db.relationship('User', backref=db.backref('comments', lazy=True))
sar_call_id = db.Column(db.Integer, db.ForeignKey('sar_call.id'), nullable=False) sar_call_id = db.Column(db.Integer, db.ForeignKey('sar_call.id'), nullable=False)
sar_call = db.relationship('SARCall', backref=db.backref('comments', lazy=True)) sar_call = db.relationship('SARCall', backref=db.backref('comments', lazy=True))
class FileAttachment(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_name = db.Column(db.String(255), nullable=False)
file_type = db.Column(db.String(255), nullable=False)
file_path = db.Column(db.String(255), nullable=False)
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False)
def is_image(self):
return self.file_type in ['image/jpeg', 'image/png', 'image/gif']

29
qodana.yaml Normal file
View File

@ -0,0 +1,29 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-python:latest

View File

@ -12,4 +12,5 @@ gpxpy==1.6.1
geopy==2.4.0 geopy==2.4.0
flask_babel==4.0.0 flask_babel==4.0.0
#flask_debugtoolbar==0.13.1 #flask_debugtoolbar==0.13.1
pillow==10.1.0

253
sar_call_details.py Normal file
View File

@ -0,0 +1,253 @@
import os
from PIL import Image
from flask import request, redirect, flash, render_template, url_for, jsonify, Response, send_from_directory
from flask_login import login_required, current_user
from sqlalchemy import and_
from sqlalchemy.orm import aliased
from werkzeug.utils import secure_filename
from app import app, db
from models import SARCall, Comment, GPSTrack, SARCategory, SARStatus, User, SARResult, FileAttachment
import base64
@app.route('/sar_details/<int:id>')
def sar_details(id):
is_logged_in = current_user.is_authenticated
search_officer = aliased(User)
coordination_officer = aliased(User)
sar = (SARCall.query
.outerjoin(search_officer,
and_(SARCall.search_officer_id == search_officer.id, SARCall.search_officer_id != None))
.join(coordination_officer, SARCall.coordination_officer_id == coordination_officer.id)
.join(SARCategory, SARCall.category == SARCategory.id)
.join(SARStatus, SARCall.status == SARStatus.id)
.outerjoin(SARResult, and_(SARCall.result == SARResult.id, SARCall.result != None))
.add_columns(SARCall, SARCategory, SARStatus, SARResult)
.filter(SARCall.id == id).first())
comments = Comment.query.filter_by(sar_call_id=id).all()
gpx_files = [id[0] for id in GPSTrack.query.with_entities(GPSTrack.id).filter_by(
sar_call_id=id).all()] # Fetch all GPX files for this SARCall
comments_with_gpx = []
comments_with_attachment = []
for comment in comments:
gpx_tracks = GPSTrack.query.filter_by(comment_id=comment.id).all()
for track in gpx_tracks:
comments_with_gpx.append({
"id": track.id,
"comment_id": comment.id,
"name": track.file_name,
"comment": track.gpx_name
})
attachments = FileAttachment.query.filter_by(comment_id=comment.id).all()
for attachment in attachments:
comments_with_attachment.append({
"id": attachment.id,
"comment_id": comment.id,
"file_name": attachment.file_name,
"file_type": attachment.file_type,
"file_path": attachment.file_path,
"is_image": attachment.is_image()
})
return render_template('sar_details.html', sar=sar,
gpx_ids=gpx_files,
comments_with_gpx=comments_with_gpx,
comments_with_attachment=comments_with_attachment,
is_logged_in=is_logged_in, filename_prefix=app.config['STORAGE_DIR'] + '/')
@app.route('/add_comment/<int:sar_call_id>', methods=['POST'])
@login_required
def add_comment(sar_call_id):
text = request.form.get('text')
comment = Comment(text=text, user_id=current_user.id, sar_call_id=sar_call_id)
db.session.add(comment)
db.session.commit()
return redirect(url_for('sar_details', id=sar_call_id))
@app.route('/edit_comment/<int:comment_id>', methods=['POST'])
@login_required
def edit_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
# Permission checks...
comment_text = request.form.get('comment')
comment.text = comment_text
db.session.commit()
# return jsonify(success=True) # or return relevant response
return redirect(url_for('sar_details', id=comment.sar_call_id))
@app.route('/delete_comment/<int:id>', methods=['GET', 'POST'])
@login_required
def delete_comment(id):
comment = Comment.query.get_or_404(id)
# if current_user.id != comment.user_id and current_user.id != 1 and current_user.id != comment.sar_call.user_id:
# abort(403)
# delete associated GPX files
gpx_tracks = GPSTrack.query.filter_by(comment_id=comment.id).all()
for track in gpx_tracks:
db.session.delete(track)
db.session.delete(comment)
db.session.commit()
flash('Comment deleted successfully!', 'success')
return redirect(url_for('sar_details', id=comment.sar_call_id))
@app.route('/upload_gpx', methods=['POST'])
@login_required
def upload_gpx():
# Retrieve file and other data from the form
file_name = request.form.get('gpxFileName')
gpx_file = request.files.get('gpxFile')
id = request.form.get('commentId')
sar_id = request.form.get('sarId')
# You need to implement logic to parse and store the GPX file
# For example, read the file content
gpx_data = gpx_file.read()
# Create a new GPSTrack object and save it
new_gpx_file = GPSTrack(comment_id=id, sar_call_id=sar_id, file_name=file_name, gpx_data=gpx_data)
db.session.add(new_gpx_file)
db.session.commit()
return jsonify({'message': 'GPX file uploaded successfully'})
def custom_flask_response(data, status=200, headers=None, mimetype='application/json'):
# TODO: fix filename encoding -- need to support unicode
if headers is not None:
new_headers = {}
for key, value in headers.items():
new_key = base64.b64encode(str(key).encode('utf-8'))
new_value = base64.b64encode(str(value).encode('utf-8'))
new_headers[new_key] = new_value
headers = new_headers
return Response(data, status=status, headers=headers, mimetype=mimetype)
@app.route('/get_gpx/<int:gpx_id>')
def get_gpx(gpx_id):
gpx_file = GPSTrack.query.get_or_404(gpx_id)
return custom_flask_response(gpx_file.gpx_data, mimetype='application/gpx+xml',
headers={'Content-Disposition': 'attachment;filename=' + gpx_file.file_name + '.gpx'})
@app.route('/save_track', methods=['POST'])
@login_required
def save_track():
# Get the track data from the POST request
track_data = request.form.get('track_data') # Replace with the actual field name for the track data
# Get the track name and comment from the POST request
track_name = request.form.get('track_name')
track_comment = request.form.get('track_comment')
sar_id = request.form.get('sar_call_id')
# Create a new Comment instance associated with the track and save it to the database
new_comment = Comment(sar_call_id=sar_id, user_id=current_user.id, text=track_comment)
db.session.add(new_comment)
db.session.commit()
# Create a new GPXTrack instance and save it to the database
new_track = GPSTrack(comment_id=new_comment.id, sar_call_id=sar_id, file_name=track_name, gpx_data=track_data)
db.session.add(new_track)
db.session.commit()
return jsonify(success=True, message="Track saved successfully")
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def create_thumbnail(input_path, output_path, base_width=150):
img = Image.open(input_path)
w_percent = (base_width / float(img.size[0]))
h_size = int((float(img.size[1]) * float(w_percent)))
img = img.resize((base_width, h_size))
img.save(output_path)
@app.route('/upload_file', methods=['POST'])
def upload_file():
file = request.files['file']
sar_id = request.form.get('sarId')
comment_id = request.form.get('commentId')
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['STORAGE_DIR'], filename)
file.save(file_path)
# After saving the original file
if file.content_type.startswith('image/'):
thumbnail_path = os.path.join(app.config['STORAGE_DIR'], "thumbs", filename)
create_thumbnail(file_path, thumbnail_path)
file_type = file.content_type
# Create a new file attachment record
attachment = FileAttachment(file_name=filename, file_type=file_type, file_path=file_path, comment_id=comment_id)
db.session.add(attachment)
db.session.commit()
flash('File uploaded successfully!', 'success')
return redirect(url_for('sar_details', id=sar_id))
# return jsonify(success=True, message='File uploaded successfully')
flash('File upload failed!', 'danger')
return redirect(url_for('sar_details', id=sar_id))
@app.route('/delete_file/<int:attachment_id>', methods=['GET', 'POST'])
def delete_file(attachment_id):
attachment = FileAttachment.query.get(attachment_id)
sar_id = request.form.get('sarId')
if attachment:
try:
os.remove(os.path.join(app.config['STORAGE_DIR'], attachment.file_name))
try:
os.remove(os.path.join(app.config['STORAGE_DIR'], 'thumbs', attachment.file_name)) # If a thumbnail exists
except:
flash('File thumbnail not deleted', 'danger')
db.session.delete(attachment)
db.session.commit()
flash('File deleted successfully!', 'danger')
return redirect(url_for('sar_details', id=sar_id))
except Exception as e:
return redirect(url_for('sar_details', id=sar_id))
flash('File not deleted', 'danger')
return redirect(url_for('sar_details', id=sar_id))
@app.route('/download_attachment/<string:filename>')
@login_required
def download_attachment(filename):
# Implement code to serve the attachment for download
# You may use Flask's send_from_directory or send_file
return send_from_directory(app.config['STORAGE_DIR'], filename)
@app.route('/download_thumb/<string:filename>')
@login_required
def download_thumb(filename):
# Implement code to serve the attachment for download
# You may use Flask's send_from_directory or send_file
return send_from_directory(app.config['STORAGE_DIR'] + "/thumbs/", filename)

View File

@ -1,11 +1,78 @@
from dateutil import parser from flask import render_template, request, redirect, url_for, flash
from flask import request, redirect, flash, render_template, url_for, jsonify, Response from flask_login import current_user, login_required
from flask_login import login_required, current_user from sqlalchemy import and_, or_
from sqlalchemy import or_, and_
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from dateutil import parser
from app import app, db from app import app, db
from models import SARCall, Comment, GPSTrack, SARCategory, SARStatus, User, Role, SARResult from models import SARCall, SARStatus, SARCategory, User, Role, SARResult
# This Python Flask module provides the functionality to list Search and Rescue (SAR) operations stored in the
# database. Routes included in this file: /list_sar A route that loads and returns the 'list_sar.html' template to
# display SAR operations. /update_sar_list A route that dynamically updates the SAR operations list based on user
# interactions such as filtering and sorting the list. The 'render_sar_list_template' function is a helper function
# that is used by these two routes to reduce code repetition. Functions: render_sar_list_template(template) A shared
# function for the '/list_sar' and '/update_sar_list' routes. It queries the database according to user's selected
# filters and sort orders, and renders the given template with the SAR operation data. It uses a SQLAlchemy query to
# obtain the SAR operation records. The records can be filtered by status and category, and can be sorted by date or
# by operation ID. Parameters: template (str): The name of the HTML template to render. Returns: A rendered Flask
# template, ready to be sent to the client's browser. This module uses a number of models from an imported models
# module: SARCall: A model representing a SAR operation. SARStatus: A model representing possible statuses of a SAR
# operation. SARCategory: A model representing possible categories of a SAR operation thus allowing classification.
# User: A model representing a logged-in user.
@app.route('/list_sar')
def list_sar():
return render_sar_list_template('list_sar.html')
@app.route('/update_sar_list/')
def update_sar_list():
return render_sar_list_template('dynamic-sar-list.html')
def render_sar_list_template(template):
is_logged_in = current_user.is_authenticated
search_officer = aliased(User)
coordination_officer = aliased(User)
categories = SARCategory.query.all()
statuses = SARStatus.query.all()
category_id = request.args.get('category')
sort_order = request.args.get('sort')
status_id = request.args.get('status')
query = SARCall.query
# Filter by status
if status_id:
query = query.filter_by(status=status_id)
# Filter by category
if category_id:
query = query.filter_by(category=category_id)
# Sorting
if sort_order == 'date_asc':
query = query.order_by(SARCall.start_date.asc())
elif sort_order == 'date_desc':
query = query.order_by(SARCall.start_date.desc())
# add other sorting options if needed
else:
query = query.order_by(SARCall.id.desc())
sar_calls = (query
.outerjoin(search_officer,
and_(SARCall.search_officer_id == search_officer.id, SARCall.search_officer_id is not None))
.join(coordination_officer, SARCall.coordination_officer_id == coordination_officer.id)
.join(SARCategory, SARCall.category == SARCategory.id)
.join(SARStatus, SARCall.status == SARStatus.id)
.add_columns(SARCategory, SARCall, SARStatus)
.all())
return render_template(template, sar_calls=sar_calls, is_logged_in=is_logged_in,
categories=categories,
statuses=statuses)
@app.route('/create_sar', methods=['GET', 'POST']) @app.route('/create_sar', methods=['GET', 'POST'])
@ -43,54 +110,10 @@ def create_sar():
return render_template('create_sar.html', categories=categories, statuses=statuses, managers=managers) return render_template('create_sar.html', categories=categories, statuses=statuses, managers=managers)
@app.route('/list_sar') @app.route('/edit_sar/<int:sar_id>', methods=['GET', 'POST'])
def list_sar():
is_logged_in = current_user.is_authenticated
search_officer = aliased(User)
coordination_officer = aliased(User)
categories = SARCategory.query.all()
statuses = SARStatus.query.all()
category_id = request.args.get('category')
sort_order = request.args.get('sort')
status_id = request.args.get('status')
query = SARCall.query
# Filter by status
if status_id:
query = query.filter_by(status=status_id)
# Filter by category
if category_id:
query = query.filter_by(category=category_id)
# Sorting
if sort_order == 'date_asc':
query = query.order_by(SARCall.start_date.asc())
elif sort_order == 'date_desc':
query = query.order_by(SARCall.start_date.desc())
# add other sorting options if needed
else:
query = query.order_by(SARCall.id.desc())
sar_calls = (query
.outerjoin(search_officer,
and_(SARCall.search_officer_id == search_officer.id, SARCall.search_officer_id != None))
.join(coordination_officer, SARCall.coordination_officer_id == coordination_officer.id)
.join(SARCategory, SARCall.category == SARCategory.id)
.join(SARStatus, SARCall.status == SARStatus.id)
.add_columns(SARCategory, SARCall, SARStatus)
.all())
return render_template('list_sar.html', sar_calls=sar_calls, is_logged_in=is_logged_in, categories=categories, statuses=statuses)
@app.route('/edit_sar/<int:id>', methods=['GET', 'POST'])
@login_required @login_required
def edit_sar(id): def edit_sar(sar_id):
sar_call = SARCall.query.get_or_404(id) sar_call = SARCall.query.get_or_404(sar_id)
categories = SARCategory.query.all() categories = SARCategory.query.all()
statuses = SARStatus.query.order_by('id').all() statuses = SARStatus.query.order_by('id').all()
results = SARResult.query.all() results = SARResult.query.all()
@ -117,7 +140,6 @@ def edit_sar(id):
sar_call.search_officer_id = request.form.get('search_officer') sar_call.search_officer_id = request.form.get('search_officer')
sar_call.coordination_officer_id = request.form.get('coordination_officer') sar_call.coordination_officer_id = request.form.get('coordination_officer')
db.session.commit() db.session.commit()
flash('SAR call updated successfully!', 'success') flash('SAR call updated successfully!', 'success')
return redirect(url_for('list_sar')) return redirect(url_for('list_sar'))
@ -132,145 +154,11 @@ def edit_sar(id):
search_officers=search_officers) search_officers=search_officers)
@app.route('/sar_details/<int:id>') @app.route('/delete_sar/<int:sar_id>')
def sar_details(id):
is_logged_in = current_user.is_authenticated
search_officer = aliased(User)
coordination_officer = aliased(User)
sar = (SARCall.query
.outerjoin(search_officer,
and_(SARCall.search_officer_id == search_officer.id, SARCall.search_officer_id != None))
.join(coordination_officer, SARCall.coordination_officer_id == coordination_officer.id)
.join(SARCategory, SARCall.category == SARCategory.id)
.join(SARStatus, SARCall.status == SARStatus.id)
.outerjoin(SARResult, and_(SARCall.result == SARResult.id, SARCall.result != None))
.add_columns(SARCall, SARCategory, SARStatus, SARResult)
.filter(SARCall.id == id).first())
comments = Comment.query.filter_by(sar_call_id=id).all()
gpx_files = [id[0] for id in GPSTrack.query.with_entities(GPSTrack.id).filter_by(
sar_call_id=id).all()] # Fetch all GPX files for this SARCall
comments_with_gpx = []
for comment in comments:
gpx_tracks = GPSTrack.query.filter_by(comment_id=comment.id).all()
for track in gpx_tracks:
comments_with_gpx.append({
"id": track.id,
"comment_id": comment.id,
"name": track.file_name,
"comment": track.gpx_name
})
return render_template('sar_details.html', sar=sar, gpx_ids=gpx_files, comments_with_gpx=comments_with_gpx,
is_logged_in=is_logged_in)
@app.route('/delete_sar/<int:id>')
@login_required @login_required
def delete_sar(id): def delete_sar(sar_id):
sar_call = SARCall.query.get_or_404(id) sar_call = SARCall.query.get_or_404(sar_id)
db.session.delete(sar_call) db.session.delete(sar_call)
db.session.commit() db.session.commit()
flash('SAR call record deleted successfully!', 'success') flash('SAR call record deleted successfully!', 'success')
return redirect(url_for('list_sar')) return redirect(url_for('list_sar'))
@app.route('/add_comment/<int:sar_call_id>', methods=['POST'])
@login_required
def add_comment(sar_call_id):
text = request.form.get('text')
comment = Comment(text=text, user_id=current_user.id, sar_call_id=sar_call_id)
db.session.add(comment)
db.session.commit()
return redirect(url_for('sar_details', id=sar_call_id))
@app.route('/edit_comment/<int:comment_id>', methods=['POST'])
@login_required
def edit_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
# Permission checks...
comment_text = request.form.get('comment')
comment.text = comment_text
db.session.commit()
# return jsonify(success=True) # or return relevant response
return redirect(url_for('sar_details', id=comment.sar_call_id))
@app.route('/delete_comment/<int:id>', methods=['GET', 'POST'])
@login_required
def delete_comment(id):
comment = Comment.query.get_or_404(id)
# if current_user.id != comment.user_id and current_user.id != 1 and current_user.id != comment.sar_call.user_id:
# abort(403)
# delete associated GPX files
gpx_tracks= GPSTrack.query.filter_by(comment_id=comment.id).all()
for track in gpx_tracks:
db.session.delete(track)
db.session.delete(comment)
db.session.commit()
flash('Comment deleted successfully!', 'success')
return redirect(url_for('sar_details', id=comment.sar_call_id))
@app.route('/upload_gpx', methods=['POST'])
@login_required
def upload_gpx():
# Retrieve file and other data from the form
file_name = request.form.get('gpxFileName')
gpx_file = request.files.get('gpxFile')
id = request.form.get('commentId')
sar_id = request.form.get('sarId')
# You need to implement logic to parse and store the GPX file
# For example, read the file content
gpx_data = gpx_file.read()
# Create a new GPSTrack object and save it
new_gpx_file = GPSTrack(comment_id=id, sar_call_id=sar_id, file_name=file_name, gpx_data=gpx_data)
db.session.add(new_gpx_file)
db.session.commit()
return jsonify({'message': 'GPX file uploaded successfully'})
@app.route('/get_gpx/<int:gpx_id>')
def get_gpx(gpx_id):
gpx_file = GPSTrack.query.get_or_404(gpx_id)
return Response(gpx_file.gpx_data, mimetype='application/gpx+xml',
headers={'Content-Disposition': 'attachment;filename=' + gpx_file.file_name + '.gpx'})
@app.route('/save_track', methods=['POST'])
@login_required
def save_track():
# Get the track data from the POST request
track_data = request.form.get('track_data') # Replace with the actual field name for the track data
# Get the track name and comment from the POST request
track_name = request.form.get('track_name')
track_comment = request.form.get('track_comment')
sar_id = request.form.get('sar_call_id')
# Create a new Comment instance associated with the track and save it to the database
new_comment = Comment(sar_call_id=sar_id, user_id=current_user.id, text=track_comment)
# comment = Comment(text=text, user_id=current_user.id, sar_call_id=sar_call_id)
db.session.add(new_comment)
db.session.commit()
# Create a new GPXTrack instance and save it to the database
new_track = GPSTrack(comment_id=new_comment.id, sar_call_id=sar_id, file_name=track_name, gpx_data=track_data)
db.session.add(new_track)
db.session.commit()
return jsonify(success=True, message="Track saved successfully")

View File

@ -3,6 +3,10 @@
<head> <head>
<!-- ... meta tags, styles, etc. ... --> <!-- ... meta tags, styles, etc. ... -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<title>{% block title %}Default Title{% endblock %}</title> <title>{% block title %}Default Title{% endblock %}</title>
</head> </head>
@ -89,15 +93,9 @@
<!-- ... footer content ... --> <!-- ... footer content ... -->
</footer> </footer>
<!-- Scripts -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script> <script>
$(document).ready(function () { jQuery(document).ready(function ($) {
$(".dropdown-toggle").dropdown(); $(".dropdown-toggle").dropdown();
}); });
</script> </script>

View File

@ -0,0 +1,87 @@
<div class="container mt-5">
<h2>SAR Records</h2>
<form method="get" action="{{ url_for('list_sar') }}" id="filter-form">
<!-- Filter by Status -->
<select id="status-select" name="status" onchange="submitForm()">
<option value="">All Statuses</option>
<!-- Populate options with statuses -->
{% for status in statuses %}
<option value="{{ status.id }}">{{ status.name }}</option>
{% endfor %}
</select>
<!-- Filter by Category -->
<select id="category-select" name="category" onchange="submitForm()">
<option value="">All Categories</option>
<!-- Populate options with categories -->
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
<!-- Sorting Options -->
<select id="sort-select" name="sort" onchange="submitForm()">
<option value="date_asc">Date (Oldest First)</option>
<option value="date_desc">Date (Newest First)</option>
<!-- other sorting options -->
</select>
</form>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Status</th>
<th>Start Date</th>
<th>Finish Date</th>
<th>Category</th>
<th>Created by</th>
<th>Manager</th>
{% if is_logged_in %}
<th>Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for sar in sar_calls %}
<tr class="clickable-row" data-href="{{ url_for('sar_details', id=sar.SARCall.id) }}">
<td>{{ sar.SARCall.id }}</td>
<td>{{ sar.SARCall.title }}</td>
<td>{{ sar.SARStatus.name }}</td>
<td>{{ sar.SARCall.start_date }}</td>
<td>{{ sar.SARCall.finish_date }}</td>
<td>{{ sar.SARCategory.name }}</td>
<td>{{ sar.SARCall.coordination_officer.full_name }}</td>
<td>{{ sar.SARCall.search_officer.full_name }}</td>
{% if is_logged_in %}
<td>
<a href="{{ url_for('edit_sar', sar_id=sar.SARCall.id) }}">
<button type="button" class="btn btn-info">&#9998;</button>
{# Edit button #}
</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
document.querySelectorAll('.clickable-row').forEach(row => {
row.addEventListener('click', () => {
window.location.href = row.dataset.href;
});
});
</script>
<script>
function submitForm() {
document.getElementById("filter-form").submit();
}
</script>

View File

@ -12,7 +12,7 @@
<h2>Edit SAR Call</h2> <h2>Edit SAR Call</h2>
<div class="container mt-5"> <div class="container mt-5">
<h2>Edit SAR Call</h2> <h2>Edit SAR Call</h2>
<form action="{{ url_for('edit_sar', id=sar_call.id) }}" method="post" enctype="multipart/form-data"> <form action="{{ url_for('edit_sar', sar_id=sar_call.id) }}" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="form-group">
<div> <div>
<label for="status">Status:</label> <label for="status">Status:</label>

View File

@ -4,99 +4,29 @@
SAR Records SAR Records
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container mt-5"> <div id="dynamic-sar-list">
<h2>SAR Records</h2>
<form method="get" action="{{ url_for('list_sar') }}" id="filter-form"> {% include 'dynamic-sar-list.html' %}
<!-- Filter by Status -->
<select id="status-select" name="status" onchange="submitForm()">
<option value="">All Statuses</option>
<!-- Populate options with statuses -->
{% for status in statuses %}
<option value="{{ status.id }}">{{ status.name }}</option>
{% endfor %}
</select>
<!-- Filter by Category -->
<select id="category-select" name="category" onchange="submitForm()">
<option value="">All Categories</option>
<!-- Populate options with categories -->
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
<!-- Sorting Options -->
<select id="sort-select" name="sort" onchange="submitForm()">
<option value="date_asc">Date (Oldest First)</option>
<option value="date_desc">Date (Newest First)</option>
<!-- other sorting options -->
</select>
</form>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Status</th>
<th>Start Date</th>
<th>Finish Date</th>
<th>Category</th>
<th>Created by</th>
<th>Manager</th>
{% if is_logged_in %}
<th>Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for sar in sar_calls %}
<tr class="clickable-row" data-href="{{ url_for('sar_details', id=sar.SARCall.id) }}">
<td>{{ sar.SARCall.id }}</td>
<td>{{ sar.SARCall.title }}</td>
<td>{{ sar.SARStatus.name }}</td>
<td>{{ sar.SARCall.start_date }}</td>
<td>{{ sar.SARCall.finish_date }}</td>
<td>{{ sar.SARCategory.name }}</td>
<td>{{ sar.SARCall.coordination_officer.full_name }}</td>
<td>{{ sar.SARCall.search_officer.full_name }}</td>
{% if is_logged_in %}
<td>
<a href="{{ url_for('edit_sar', id=sar.SARCall.id) }}">
<button type="button" class="btn btn-info">Edit</button>
</a>
{# <a href="{{ url_for('delete_sar', id=sar.SARCall.id) }}">#}
{# <button type="button" class="btn btn-danger">Delete</button>#}
{# </a>#}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{# <a href="/dashboard">Back to Dashboard</a>#}
</div> </div>
<script> <script>
document.querySelectorAll('.clickable-row').forEach(row => { $(document).ready(function() {
row.addEventListener('click', () => { function updateSarDetails() {
window.location.href = row.dataset.href; $.ajax({
}); url: '/update_sar_list',
}); type: 'GET',
</script> success: function(data) {
$('#dynamic-sar-list').html(data); // Replace the content of the div
<script>
function submitForm() {
document.getElementById("filter-form").submit();
} }
</script> });
}
updateSarDetails(); // Do it once immediately
setInterval(updateSarDetails, 5000); // Then every 5 seconds
});
</script>
{% endblock %} {% endblock %}

View File

@ -11,7 +11,6 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css">
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<!-- Leaflet.draw JS --> <!-- Leaflet.draw JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<!-- Include leaflet-gpx plugin --> <!-- Include leaflet-gpx plugin -->
@ -77,15 +76,17 @@
<!-- Other SAR details --> <!-- Other SAR details -->
</tr> </tr>
</table> </table>
<div>
{% if is_logged_in %}
<a href="{{ url_for('edit_sar', sar_id=sar.SARCall.id) }}" class="btn btn-primary"> &#9998;</a>
{# EDIT #}
<a href="{{ url_for('delete_sar', sar_id=sar.SARCall.id) }}"
class="btn btn-danger">&#128465;</a> {# DELETE #}
{% endif %}
</div>
</div> </div>
</div> </div>
<div>
{% if is_logged_in %}
<a href="{{ url_for('edit_sar', id=sar.SARCall.id) }}" class="btn btn-primary">Edit</a>
<a href="{{ url_for('delete_sar', id=sar.SARCall.id) }}" class="btn btn-danger">Delete</a>
{% endif %}
</div>
<div id="map" class="mb-4"></div> <div id="map" class="mb-4"></div>
@ -95,44 +96,127 @@
<div class="comment"> <div class="comment">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<strong>{{ comment.user.full_name }}</strong>: at {{ comment.created }} , <small> Comment # {{ comment.id }} by <strong>{{ comment.user.full_name }}</strong>
updated {{ comment.updated }} at {{ comment.created }},
updated {{ comment.updated }} </small>
<div class="card mb-5"> <div class="card mb-5">
{# Comment text #}
<div class="card-body"> <div class="card-body">
<div class="comment-text"><p id="comment-text-{{ comment.id }}">{{ comment.text }}</p> <div class="comment-text"><p id="comment-text-{{ comment.id }}">{{ comment.text }}</p>
</div> </div>
</div>
</div>
{% for gpx_track in comments_with_gpx %}
{% if gpx_track.comment_id == comment.id %}
<li>
<a href="{{ url_for('get_gpx', gpx_id=gpx_track.id) }}">{{ gpx_track.name }}</a>: {{ gpx_track.comment }}
</li>
{% endif %}
{% endfor %}
{% if is_logged_in %} {% if is_logged_in %}
{% if current_user.id == comment.user_id or current_user.id == 1 or current_user.id == sar.SARCall.user_id %} {% if current_user.id == comment.user_id or current_user.id == 1 or current_user.id == sar.SARCall.user_id %}
<button class="edit-comment-btn" data-comment-id="{{ comment.id }}" <button class="edit-comment-btn" data-comment-id="{{ comment.id }}"
data-comment-text="{{ comment.text }}">Edit data-comment-text="{{ comment.text }}"> &#9998; {# EDIT #}
</button> </button>
<button type="button" class="delete-comment-btn">
<a href="{{ url_for('delete_comment', id=comment.id) }}">&#128465;</a>
{# DELETE #}
</button>
{% endif %}
</div>
{# GPX tracks part #}
<div class="card mb-6">
<div class="card-body">
{% set ns = namespace(drawn = false) %}
<small><strong>GPX track:</strong></small>
{% for gpx_track in comments_with_gpx %}
{% if gpx_track.comment_id == comment.id %}
<li>
<a href="{{ url_for('get_gpx', gpx_id=gpx_track.id) }}">{{ gpx_track.name }}</a>
</li>
{% set ns.drawn = true %}
<!-- Button to delete GPX Upload Modal -->
<button type="button" class="gpx-delete-button" data-toggle="modal"
data-target="#deleteGPXModal"
data-comment-id="{{ comment.id }}">
&#128465;
</button>
{% endif %}
{% endfor %}
{% if ns.drawn == false %}
<!-- Button to Open GPX Upload Modal --> <!-- Button to Open GPX Upload Modal -->
<button type="button" class="gpx-upload-button" data-toggle="modal" <button type="button" class="gpx-upload-button" data-toggle="modal"
data-target="#uploadGPXModal" data-target="#uploadGPXModal"
data-comment-id="{{ comment.id }}"> data-comment-id="{{ comment.id }}">
Upload GPX File Upload GPX File
</button> </button>
{% endif %}
</div>
</div>
<div class="card mb-6">
<div class="card-body">
{# File attachments part #}
<small><strong>File attachment:</strong></small><p>
{% set ns=namespace(has_file = false) %}
{% for file in comments_with_attachment %}
{% if file.comment_id == comment.id %}
{% set ns.has_file = true %}
{% if file.is_image %}
<img src="{{ url_for('download_thumb', filename=file.file_name) }}"
class="img-thumbnail">
{% set filename = file.file_name %}
{% else %}
<i class="fa fa-file" aria-hidden="true"></i> {{ file.file_name }}
{% endif %}
{% endif %}
{% endfor %}
<button type="button" class="delete-comment-btn"> {% if is_logged_in %}
<a href="{{ url_for('delete_comment', id=comment.id) }}">Delete</a> {% if ns.has_file %}
<!-- Display the attachment -->
<table>
<td>
{# Download attachment #}
<button type="submit" class="btn btn-primary">
<a href="{{ url_for('download_attachment', filename=filename) }}">&#11015;</a>
</button> </button>
</td>
<td>
<!-- Button to delete the attachment -->
<form method="POST"
action="{{ url_for('delete_file', attachment_id=comment.id) }}">
{# Delete attachment #}
<input type="hidden" id="sar_call_id" name="sarId"
value="{{ sar.SARCall.id }}">
<button type="submit" class="btn btn-danger">&#128465;</button>
</form>
</td>
</table>
{% else %}
<form method="POST"
action={{ url_for("upload_file") }} enctype="multipart/form-data">
<input type="file" name="file" accept="image/*">
<br><br>
<input type="submit" value="Upload">
<input type="file" id="attachmentInput" style="display: none;">
<input type="hidden" id="comment_id" name="commentId"
value="{{ comment.id }}">
<input type="hidden" id="sar_call_id" name="sarId"
value="{{ sar.SARCall.id }}">
</form>
<script>
// When the "Upload Attachment" button is clicked, trigger the file input
document.getElementById('uploadButton').addEventListener('click', function () {
document.getElementById('attachmentInput').click();
});
</script>
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
{% endfor %} {% endfor %}
<!-- Edit Comment Modal --> <!-- Edit Comment Modal -->
@ -168,7 +252,7 @@
<form action="{{ url_for('add_comment', sar_call_id=sar.SARCall.id) }}" method="post" <form action="{{ url_for('add_comment', sar_call_id=sar.SARCall.id) }}" method="post"
enctype="multipart/form-data"> enctype="multipart/form-data">
<div class="form-group"> <div class="form-group">
<label for="text">Comment:</label> <label for="text"><small>Add comment:</small></label>
<textarea name="text" class="form-control"></textarea> <textarea name="text" class="form-control"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -212,13 +296,7 @@
</div> </div>
{# <!-- Button to trigger the modal -->#} <!-- The GPX upload modal -->
{# <button id="save-track-button" type="button" class="btn btn-primary" data-toggle="modal"#}
{# data-target="#saveTrackModal">#}
{# Save Track#}
{# </button>#}
<!-- The Modal -->
<div class="modal fade" id="saveTrackModal" tabindex="-1" role="dialog" aria-labelledby="saveTrackModalLabel" <div class="modal fade" id="saveTrackModal" tabindex="-1" role="dialog" aria-labelledby="saveTrackModalLabel"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
@ -401,6 +479,13 @@
$('#commentIdForGPX').val(commentId); $('#commentIdForGPX').val(commentId);
}); });
$('#uploadGPXModal').on('hidden.bs.modal', function (e) {
// Refresh the parent page (assuming you want to refresh the entire page)
console.log("page refresh!!!");
location.reload(); // This will refresh the current page
});
$(document).ready(function () { $(document).ready(function () {
{#commentId = $(this).data('comment-id');#} {#commentId = $(this).data('comment-id');#}
{#console.log(commentId);#} {#console.log(commentId);#}
@ -427,10 +512,7 @@
}); });
}); });
$('#uploadGPXModal').on('hie', function (e) {
// Refresh the parent page (assuming you want to refresh the entire page)
location.reload(); // This will refresh the current page
});
</script> </script>
<script> <script>
@ -451,14 +533,10 @@
if (layer instanceof L.Circle) { if (layer instanceof L.Circle) {
feature.properties.radius = layer.getRadius(); feature.properties.radius = layer.getRadius();
drItems.features.push(feature); drItems.features.push(feature);
} } else if (layer instanceof L.CircleMarker) {
else if(layer instanceof L.CircleMarker)
{
feature.properties.radius = layer.getRadius(); feature.properties.radius = layer.getRadius();
drItems.features.push(feature); drItems.features.push(feature);
} } else {
else
{
drItems.features.push(feature); drItems.features.push(feature);
} }
}); });
@ -502,8 +580,8 @@
"features": convertedFeatures "features": convertedFeatures
}; };
console.log(convertedGeoJSON); console.log(convertedGeoJSON);
var gpx = togpx(convertedGeoJSON); // Convert GeoJSON to GPX
return gpx; return togpx(convertedGeoJSON);
} }