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 name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/storage" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<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_login import current_user
|
||||
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):
|
||||
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(Comment, 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(SARCategory, 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['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'mysql+mysqlconnector://sarbaseuser:password@localhost/sarbaseapp')
|
||||
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
|
||||
#toolbar = DebugToolbarExtension(app)
|
||||
babel= Babel(app)
|
||||
@ -37,11 +44,11 @@ migrate = Migrate(app, db)
|
||||
login_manager = LoginManager(app)
|
||||
|
||||
|
||||
|
||||
import models
|
||||
import admin
|
||||
import login
|
||||
import sar_calls
|
||||
import sar_call_details
|
||||
import dashboard
|
||||
|
||||
|
||||
|
@ -10,8 +10,10 @@ services:
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
- DATABASE_URL=mysql+mysqlconnector://sarbaseuser:password@db/sarbaseapp
|
||||
- DOCKER_ENV=true
|
||||
volumes:
|
||||
- ./certs:/certs
|
||||
- ./storage:/storage
|
||||
|
||||
nginx:
|
||||
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))
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
flask_babel==4.0.0
|
||||
#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 request, redirect, flash, render_template, url_for, jsonify, Response
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import or_, and_
|
||||
from flask import render_template, request, redirect, url_for, flash
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import aliased
|
||||
from dateutil import parser
|
||||
|
||||
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'])
|
||||
@ -43,54 +110,10 @@ def create_sar():
|
||||
return render_template('create_sar.html', categories=categories, statuses=statuses, managers=managers)
|
||||
|
||||
|
||||
@app.route('/list_sar')
|
||||
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'])
|
||||
@app.route('/edit_sar/<int:sar_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_sar(id):
|
||||
sar_call = SARCall.query.get_or_404(id)
|
||||
def edit_sar(sar_id):
|
||||
sar_call = SARCall.query.get_or_404(sar_id)
|
||||
categories = SARCategory.query.all()
|
||||
statuses = SARStatus.query.order_by('id').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.coordination_officer_id = request.form.get('coordination_officer')
|
||||
|
||||
|
||||
db.session.commit()
|
||||
flash('SAR call updated successfully!', 'success')
|
||||
return redirect(url_for('list_sar'))
|
||||
@ -132,145 +154,11 @@ def edit_sar(id):
|
||||
search_officers=search_officers)
|
||||
|
||||
|
||||
@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 = []
|
||||
|
||||
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>')
|
||||
@app.route('/delete_sar/<int:sar_id>')
|
||||
@login_required
|
||||
def delete_sar(id):
|
||||
sar_call = SARCall.query.get_or_404(id)
|
||||
def delete_sar(sar_id):
|
||||
sar_call = SARCall.query.get_or_404(sar_id)
|
||||
db.session.delete(sar_call)
|
||||
db.session.commit()
|
||||
flash('SAR call record deleted successfully!', 'success')
|
||||
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>
|
||||
<!-- ... 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>
|
||||
</head>
|
||||
@ -89,15 +93,9 @@
|
||||
<!-- ... footer content ... -->
|
||||
</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>
|
||||
$(document).ready(function () {
|
||||
jQuery(document).ready(function ($) {
|
||||
$(".dropdown-toggle").dropdown();
|
||||
});
|
||||
</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>
|
||||
<div class="container mt-5">
|
||||
<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>
|
||||
<label for="status">Status:</label>
|
||||
|
@ -4,99 +4,29 @@
|
||||
SAR Records
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h2>SAR Records</h2>
|
||||
<div id="dynamic-sar-list">
|
||||
|
||||
<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>
|
||||
{% include 'dynamic-sar-list.html' %}
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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();
|
||||
$(document).ready(function() {
|
||||
function updateSarDetails() {
|
||||
$.ajax({
|
||||
url: '/update_sar_list',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
$('#dynamic-sar-list').html(data); // Replace the content of the div
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
updateSarDetails(); // Do it once immediately
|
||||
setInterval(updateSarDetails, 5000); // Then every 5 seconds
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -11,7 +11,6 @@
|
||||
<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://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||
<!-- Leaflet.draw JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
<!-- Include leaflet-gpx plugin -->
|
||||
@ -77,15 +76,17 @@
|
||||
<!-- Other SAR details -->
|
||||
</tr>
|
||||
</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>
|
||||
{% 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>
|
||||
|
||||
@ -95,44 +96,127 @@
|
||||
<div class="comment">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<strong>{{ comment.user.full_name }}</strong>: at {{ comment.created }} ,
|
||||
updated {{ comment.updated }}
|
||||
<small> Comment # {{ comment.id }} by <strong>{{ comment.user.full_name }}</strong>
|
||||
at {{ comment.created }},
|
||||
updated {{ comment.updated }} </small>
|
||||
<div class="card mb-5">
|
||||
{# Comment text #}
|
||||
<div class="card-body">
|
||||
|
||||
<div class="comment-text"><p id="comment-text-{{ comment.id }}">{{ comment.text }}</p>
|
||||
</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 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 }}"
|
||||
data-comment-text="{{ comment.text }}">Edit
|
||||
data-comment-text="{{ comment.text }}"> ✎ {# EDIT #}
|
||||
</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 type="button" class="gpx-upload-button" data-toggle="modal"
|
||||
data-target="#uploadGPXModal"
|
||||
data-comment-id="{{ comment.id }}">
|
||||
Upload GPX File
|
||||
</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">
|
||||
<a href="{{ url_for('delete_comment', id=comment.id) }}">Delete</a>
|
||||
{% if is_logged_in %}
|
||||
{% 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>
|
||||
</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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Edit Comment Modal -->
|
||||
@ -168,7 +252,7 @@
|
||||
<form action="{{ url_for('add_comment', sar_call_id=sar.SARCall.id) }}" method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="text">Comment:</label>
|
||||
<label for="text"><small>Add comment:</small></label>
|
||||
<textarea name="text" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -212,13 +296,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
{# <!-- Button to trigger the modal -->#}
|
||||
{# <button id="save-track-button" type="button" class="btn btn-primary" data-toggle="modal"#}
|
||||
{# data-target="#saveTrackModal">#}
|
||||
{# Save Track#}
|
||||
{# </button>#}
|
||||
|
||||
<!-- The Modal -->
|
||||
<!-- The GPX upload modal -->
|
||||
<div class="modal fade" id="saveTrackModal" tabindex="-1" role="dialog" aria-labelledby="saveTrackModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
@ -401,6 +479,13 @@
|
||||
$('#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 () {
|
||||
{#commentId = $(this).data('comment-id');#}
|
||||
{#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>
|
||||
@ -451,14 +533,10 @@
|
||||
if (layer instanceof L.Circle) {
|
||||
feature.properties.radius = layer.getRadius();
|
||||
drItems.features.push(feature);
|
||||
}
|
||||
else if(layer instanceof L.CircleMarker)
|
||||
{
|
||||
} else if (layer instanceof L.CircleMarker) {
|
||||
feature.properties.radius = layer.getRadius();
|
||||
drItems.features.push(feature);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
drItems.features.push(feature);
|
||||
}
|
||||
});
|
||||
@ -502,8 +580,8 @@
|
||||
"features": convertedFeatures
|
||||
};
|
||||
console.log(convertedGeoJSON);
|
||||
var gpx = togpx(convertedGeoJSON);
|
||||
return gpx;
|
||||
// Convert GeoJSON to GPX
|
||||
return togpx(convertedGeoJSON);
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user