1) added dynamic SAR calls list reloading
2) added support for uploading images and files 3) refactored sar_details
This commit is contained in:
parent
a9b074a42b
commit
a33c77d2d0
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/storage/
|
||||||
|
/instance/
|
||||||
|
/migrations/
|
||||||
|
/certs/
|
||||||
|
/.idea/
|
@ -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" />
|
||||||
|
3
admin.py
3
admin.py
@ -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
9
app.py
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
11
models.py
11
models.py
@ -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
29
qodana.yaml
Normal 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
|
@ -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
253
sar_call_details.py
Normal 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)
|
268
sar_calls.py
268
sar_calls.py
@ -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")
|
|
||||||
|
@ -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>
|
||||||
|
87
templates/dynamic-sar-list.html
Normal file
87
templates/dynamic-sar-list.html
Normal 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">✎</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>
|
||||||
|
|
@ -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>
|
||||||
|
@ -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 %}
|
@ -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"> ✎</a>
|
||||||
|
{# EDIT #}
|
||||||
|
<a href="{{ url_for('delete_sar', sar_id=sar.SARCall.id) }}"
|
||||||
|
class="btn btn-danger">🗑</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 }}"> ✎ {# EDIT #}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="delete-comment-btn">
|
||||||
|
<a href="{{ url_for('delete_comment', id=comment.id) }}">🗑</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 }}">
|
||||||
|
🗑
|
||||||
|
</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) }}">⬇</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">🗑</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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user