TiledViz
Loading...
Searching...
No Matches
routes.py
1# -*- coding: utf-8 -*-
2
3# routes are defined here, then imported in __init__.py
4
5from flask import render_template, flash, redirect, session, request, jsonify, make_response, url_for, Response, Flask
6from flask_limiter import Limiter
7from flask_limiter.util import get_remote_address
8import markupsafe
9
10import sqlalchemy
11from sqlalchemy.orm.session import make_transient
12from sqlalchemy.orm.attributes import flag_modified
13
14from flask_sqlalchemy import SQLAlchemy
15from sqlalchemy import func
16#import requests
17
18from bs4 import BeautifulSoup
19import lxml
20
21from app import app, socketio, db
22
23from app.forms import BuildRegisterForm, BuildLoginForm, Build2FAForm, BuildNewProjectForm, BuildAllProjectSessionForm, BuildOldProjectForm, BuildNewSessionForm, BuildTilesSetForm, BuildEditsessionform, BuildOldTileSetForm, BuildConfigSessionForm, BuildConnectionsForm, BuildRetreiveSessionForm, BuildAdminForm
24
25# Import email utilities
26from app.email_utils import generate_verification_token, send_verification_email, verify_token, delete_sent_email, generate_verification_code, send_2FAcode_email, send_new_register_email
27
28import app.models as models # DB management
29import json, os, pprint
30#import distutils
31#import shutil
32# import gzip,base64
33import socket
34
35import logging
36import code
37
38from flask_socketio import emit, join_room, rooms
39from flask_cors import CORS
40from flask_cors import cross_origin
41
42#import werkzeug.exceptions
43from werkzeug.utils import secure_filename
44
45import sys,re,traceback
46import datetime,time
47import random
48
49import tempfile, filecmp
50import tarfile
51import configparser
52
53import pandas as pd
54
55sys.path.append(os.path.abspath('../TVDatabase'))
56from TVDb import tvdb
57tvdb.session=db.session
58
59logging.debug("Flask Route start app config : "+str(app.config))
60
61# - PCA -
62# importing the module analysis-and-treatment-of-data
63sys.path.append(os.path.abspath('app'))
64from Anatreada import anatreada
65
66
67# Read config file
68TVrunDir='/.tiledviz'
69TVconf=TVrunDir+"/tiledviz.conf"
70configExist=False
71if (os.path.isdir(TVrunDir)):
72 if (os.path.isfile(TVconf)):
73 configExist=True
74else:
75 os.mkdir(TVrunDir)
76 mode = os.stat(TVrunDir).st_mode
77 mode -= (mode & (stat.S_IRWXG | stat.S_IRWXO))
78 os.chmod(TVrunDir,mode)
79
80if (configExist):
81 TVconfig = configparser.ConfigParser()
82 TVconfig.optionxform = str
83 TVconfig.read(TVconf)
84
85 # default activation for 2FA
86 is2FaActivated=json.loads(TVconfig['TVWeb']['is2FaActivated'].lower())
87
88 # defaut access to HPC frontend ('ssh' for direct ssh or 'rebound' to use one or more gateways)
89 authchoice=json.loads(TVconfig['TVWeb']['AuthChoice'])
90
91 # Time sleep for waiting loops in seconds :
92 timeAlive=int(TVconfig['TVWeb']['timeAlive'])
93
94 # Time for create and start connections in seconds
95 TimeConnection=int(TVconfig['TVWeb']['TimeConnection'])
96
97 # Boolean for admin page :
98 really_delete=json.loads(TVconfig['TVWeb']['really_delete'].lower())
99
100 # Link expired duree in seconds
101 LinkExpiredAfterSec=int(TVconfig['TVWeb']['LinkExpiredAfterSec'])
102
103 # Global rate limit for all pages
104 GlobalRateLimit=json.loads(TVconfig['TVWeb']['GlobalRateLimit'])
105
106 # More vulnerable pages rate limit (register, login)
107 VulnerableRateLimit=json.loads(TVconfig['TVWeb']['VulnerableRateLimit'])
108
109else:
110 # default activation for 2FA
111 is2FaActivated=True
112
113 # defaut access to HPC frontend ('ssh' for direct ssh or 'rebound' to use one or more gateways)
114 authchoice='ssh'
115
116 # Time sleep for waiting loops in seconds :
117 timeAlive=5
118
119 # Time for create and start connections in seconds
120 TimeConnection=30
121
122 # Boolean for admin page :
123 really_delete=True
124
125 # Link expired duree in seconds
126 LinkExpiredAfterSec=240
127
128 # Global rate limit for all pages
129 GlobalRateLimit=["200 per day", "50 per hour"]
130
131 # More vulnerable pages rate limit (register, login)
132 VulnerableRateLimit="15/hour;5/minute;1/second"
133
134
135# Rate Limit
136limiter = Limiter(
137 get_remote_address,
138 app=app,
139 default_limits=GlobalRateLimit,
140 storage_uri="memory://",
141)
142
143# A global list for roles in projects
144valid_roles = ['owner', 'editor', 'viewer']
145valid_manage_members=["owner"]
146valid_manage_project=['owner', 'editor']
147
148#Separation character for invite links:
149linkChar='*'
150
151# Logging
152logFormatter = logging.Formatter("%(asctime)s - %(threadName)s - %(levelname)s: %(message)s ")
153outHandler = logging.StreamHandler(sys.stdout)
154outHandler.setLevel(logging.WARNING)
155# outHandler.setLevel(logging.DEBUG)
156outHandler.setFormatter(logFormatter)
157
158import errno,posix
159logFIFO="/tmp/logfifo"
160if (not os.path.exists(logFIFO)):
161 try:
162 os.mkfifo(logFIFO)
163 except:
164 pass
165initLOGLEV="WARNING"
166logfifo=posix.open(logFIFO, posix.O_NONBLOCK)
167LOGLEV=initLOGLEV
168def logfun(mystring):
169 global LOGLEV
170 try:
171 nLOGLEV=re.sub(r'\n',r'',posix.read(logfifo,8).decode('utf-8'))
172 except Exception as e:
173 print("Erreur LOGLEV")
174 pass
175 if (len(nLOGLEV)>2):
176 LOGLEV=nLOGLEV
177 if (LOGLEV=="WARNING"):
178 logging.warning(mystring)
179 elif (LOGLEV=="INFO"):
180 logging.info(mystring)
181 elif (LOGLEV=="DEBUG"):
182 logging.debug(mystring)
183 elif (LOGLEV=="ERROR"):
184 logging.error(mystring)
185 else:
186 logging.warning(mystring)
187 print("LOGLEV "+LOGLEV)
188
189# Shared variables for threads in server
190clients = [] # Array to store clients
191room_dict = {} # Dict to store rooms (and sub-arrays for clients in each room)
192cookie_persistence = False # Opt-in (TODO: better in config?)
193config = {}
194
195tiles_data={}
196tiles_data["nodes"]=[]
197
198jsontransfert={}
199
200# For display in form list, specify length of elements
201usernamel=str(min(20,models.Users.name.type.length))
202sessionl=str(min(60,models.Sessions.name.type.length))
203tilesetl=str(min(80,models.TileSets.name.type.length))
204projectl=str(min(15,models.Projects.name.type.length))
205connectionh=str(min(40,models.Connections.host_address.type.length))
206datel=str(19)
207descrl=str(min(62,models.Projects.description.type.length))
208
209# 2FAcode
210codes2FA={}
211
212class FormUser():
213 __slots__=["data","iseditor"]
214 def __init__(self,data,iseditor):
215 self.data=data
216 self.iseditor=iseditor
217
218# Global functions : creation, copy and delete DB elements
219
220def myflush():
221 outHandler.flush()
222 sys.stdout.flush()
223
224# Test login user :
225def get_user_id(fun,username):
226 try:
227 user=db.session.query(models.Users.id).filter_by(name=session["username"]).one()
228 except:
229 flash(fun+" requested : User must login !")
230 return redirect(url_for("login"))
231 return user[0]
232
233# Project Member Management Utility Functions
234
235def get_user_projects(user_id):
236 """
237 Get all projects for a user with their roles
238 Returns a list of tuples: (project, role_type)
239 """
240 try:
241 projects_query = db.session.query(
242 models.Projects,
243 models.ProjectMembers.role_type
244 ).join(
245 models.ProjectMembers,
246 models.Projects.id == models.ProjectMembers.project_id
247 ).filter(
248 models.ProjectMembers.user_id == user_id
249 )
250
251 return projects_query.all()
252 except Exception as e:
253 logging.error(f"Error getting user projects: {str(e)}")
254 return []
255
256def is_project_owner(project_id, user_id):
257 """
258 Check if user is owner of the project
259 """
260 try:
261 membership = db.session.query(models.ProjectMembers).filter_by(
262 project_id=project_id,
263 user_id=user_id,
264 role_type='owner'
265 ).first()
266 return membership is not None
267 except Exception as e:
268 logging.error(f"Error checking project ownership: {str(e)}")
269 return False
270
271def can_manage_project(project_id, user_id):
272 """
273 Check if user can manage the project (owner or editor)
274 """
275 try:
276 membership = db.session.query(models.ProjectMembers).filter_by(
277 project_id=project_id,
278 user_id=user_id
279 ).first()
280 return membership and membership.role_type in valid_manage_project
281 except Exception as e:
282 logging.error(f"Error checking project management permissions: {str(e)}")
283 return False
284
285
286def can_edit_project(project_id, user_id):
287 """
288 Alias helper for edit-level permissions on a project.
289 Editing is allowed for roles: owner, admin, editor.
290 """
291 return can_manage_project(project_id, user_id)
292def can_access_project(project_id, user_id):
293 """
294 Check if user can access the project (any role including viewer and guest)
295 """
296 try:
297 membership = db.session.query(models.ProjectMembers).filter_by(
298 project_id=project_id,
299 user_id=user_id
300 ).first()
301 return membership is not None
302 except Exception as e:
303 logging.error(f"Error checking project access permissions: {str(e)}")
304 return False
305
306def get_project_members(project_id):
307 """
308 Get all members of a project with their details
309 """
310 try:
311 from sqlalchemy.orm import joinedload
312
313 members = db.session.query(models.ProjectMembers).options(
314 joinedload(models.ProjectMembers.user)
315 ).filter(
316 models.ProjectMembers.project_id == project_id
317 ).order_by(
318 models.ProjectMembers.role_type,
319 models.ProjectMembers.user_id
320 ).all()
321
322 return members
323 except Exception as e:
324 logging.error(f"Error getting project members: {str(e)}")
325 return []
326
327def add_project_member(project_id, user_id, role_type='viewer'):
328 """
329 Add a user as member to a project
330 """
331 try:
332 # Check if user is already a member
333 existing_member = db.session.query(models.ProjectMembers).filter_by(
334 project_id=project_id,
335 user_id=user_id
336 ).first()
337
338 if existing_member:
339 return False, "User is already a member of this project"
340
341 # Validate role type
342 if role_type not in valid_roles:
343 return False, f"Invalid role type. Must be one of: {', '.join(valid_roles)}"
344
345 # Create new member
346 new_member = models.ProjectMembers(
347 project_id=project_id,
348 user_id=user_id,
349 role_type=role_type
350 )
351
352 db.session.add(new_member)
353 db.session.commit()
354
355 return True, "Member added successfully"
356
357 except Exception as e:
358 db.session.rollback()
359 logging.error(f"Error adding project member: {str(e)}")
360 return False, f"Error adding member: {str(e)}"
361
362def remove_project_member(project_id, user_id):
363 """
364 Remove a user from a project
365 """
366 try:
367 member = db.session.query(models.ProjectMembers).filter_by(
368 project_id=project_id,
369 user_id=user_id
370 ).first()
371
372 if not member:
373 return False, "User is not a member of this project"
374
375 # Prevent removal of the last owner
376 if member.role_type == 'owner':
377 owner_count = db.session.query(models.ProjectMembers).filter_by(
378 project_id=project_id,
379 role_type='owner'
380 ).count()
381
382 if owner_count <= 1:
383 return False, "Cannot remove the last owner of a project"
384
385 db.session.delete(member)
386 db.session.commit()
387
388 return True, "Member removed successfully"
389
390 except Exception as e:
391 db.session.rollback()
392 logging.error(f"Error removing project member: {str(e)}")
393 return False, f"Error removing member: {str(e)}"
394
395def update_member_role(project_id, user_id, new_role):
396 """
397 Update a member's role in a project
398 """
399 try:
400 member = db.session.query(models.ProjectMembers).filter_by(
401 project_id=project_id,
402 user_id=user_id
403 ).first()
404
405 if not member:
406 return False, "User is not a member of this project"
407
408 # Validate role type
409 if new_role not in valid_roles:
410 return False, f"Invalid role type. Must be one of: {', '.join(valid_roles)}"
411
412 # Prevent changing role of the last owner
413 if member.role_type == 'owner' and new_role != 'owner':
414 owner_count = db.session.query(models.ProjectMembers).filter_by(
415 project_id=project_id,
416 role_type='owner'
417 ).count()
418
419 if owner_count <= 1:
420 return False, "Cannot change role of the last owner"
421
422 member.role_type = new_role
423 db.session.commit()
424
425 return True, "Member role updated successfully"
426
427 except Exception as e:
428 db.session.rollback()
429 logging.error(f"Error updating member role: {str(e)}")
430 return False, f"Error updating role: {str(e)}"
431
432def transfer_project_ownership(project_id, current_user_id, new_owner_id):
433 """
434 Transfer project ownership to another user
435 """
436 try:
437 # Validate current user is owner
438 if not is_project_owner(project_id, current_user_id):
439 return False, "Only the project owner can transfer ownership"
440
441 # Validate new owner exists
442 new_owner = db.session.query(models.Users).filter_by(id=new_owner_id).first()
443 if not new_owner:
444 return False, "New owner user not found"
445
446 # Validate new owner is already a member
447 new_owner_membership = db.session.query(models.ProjectMembers).filter_by(
448 project_id=project_id,
449 user_id=new_owner_id
450 ).first()
451
452 if not new_owner_membership:
453 return False, "The new owner must be a member of the project first"
454
455 # Prevent self-transfer
456 if current_user_id == new_owner_id:
457 return False, "Cannot transfer ownership to yourself"
458
459 # Prevent duplicate owners
460 if new_owner_membership.role_type == 'owner':
461 return False, "The selected user is already an owner of this project"
462
463 # Perform ownership transfer
464 current_user_membership = db.session.query(models.ProjectMembers).filter_by(
465 project_id=project_id,
466 user_id=current_user_id
467 ).first()
468
469 new_owner_membership.role_type = 'owner' # Promote new owner
470
471 # Update the project's id_users field to reflect the new owner
472 project = db.session.query(models.Projects).filter_by(id=project_id).first()
473 if project:
474 project.id_users = new_owner_id
475
476 db.session.commit()
477
478 return True, "Ownership transferred successfully"
479
480 except Exception as e:
481 db.session.rollback()
482 logging.error(f"Error transferring ownership: {str(e)}")
483 return False, f"Error transferring ownership: {str(e)}"
484
485def get_available_users_for_project(project_id):
486 """
487 Get users who are not yet members of the project
488 """
489 try:
490 # Get current member IDs
491 current_member_ids = db.session.query(models.ProjectMembers.user_id).filter_by(
492 project_id=project_id
493 ).subquery()
494
495 # Get available users
496 # TODO
497 #SAWarning: Coercing Subquery object into a select() for use in IN();
498 # please pass a select() construct explicitly
499 # ~models.Users.id.in_(current_member_ids)
500 available_users = db.session.query(models.Users).filter(
501 ~models.Users.id.in_(current_member_ids)
502 ).order_by(models.Users.name).all()
503
504 return available_users
505 except Exception as e:
506 logging.error(f"Error getting available users: {str(e)}")
507 return []
508
509def sync_user_to_project_and_session(user_id, project_id, session_id, role_type='guest'):
510 """
511 Synchronize user membership between project and session
512 - Add user to project_members if not already a member
513 - Add user to many_users_has_many_sessions if not already in session
514 """
515 try:
516 # Check if user is already a project member
517 existing_member = db.session.query(models.ProjectMembers).filter_by(
518 project_id=project_id,
519 user_id=user_id
520 ).first()
521
522 if not existing_member:
523 # Add user as project member with specified role
524 new_member = models.ProjectMembers(
525 project_id=project_id,
526 user_id=user_id,
527 role_type=role_type
528 )
529 db.session.add(new_member)
530 logging.info(f"Added user {user_id} as {role_type} to project {project_id}")
531
532 # Check if user is already in session
533 existing_session_user = db.session.query(models.t_many_users_has_many_sessions).filter_by(
534 id_users=user_id,
535 id_sessions=session_id
536 ).first()
537
538 if not existing_session_user:
539 # Add user to session
540 session_user = models.t_many_users_has_many_sessions.insert().values(
541 id_users=user_id,
542 id_sessions=session_id
543 )
544 db.session.execute(session_user)
545 logging.info(f"Added user {user_id} to session {session_id}")
546
547 db.session.commit()
548 return True, "User synchronized successfully"
549
550 except Exception as e:
551 db.session.rollback()
552 logging.error(f"Error synchronizing user to project and session: {str(e)}")
553 return False, f"Error synchronizing user: {str(e)}"
554
555def validate_session_project_consistency(session_id):
556 """
557 Validate that all users in a session are members of the project
558 Returns list of inconsistencies found
559 """
560 try:
561 # Get the session and its project
562 session_obj = db.session.query(models.Sessions).filter_by(id=session_id).first()
563 if not session_obj:
564 return [f"Session {session_id} not found"]
565
566 project_id = session_obj.id_projects
567 if not project_id:
568 return [f"Session {session_id} has no associated project"]
569
570 # Get all users in the session
571 session_users = db.session.query(models.t_many_users_has_many_sessions).filter_by(
572 id_sessions=session_id
573 ).all()
574
575 inconsistencies = []
576
577 for session_user in session_users:
578 user_id = session_user.id_users
579
580 # Check if user is a project member
581 is_project_member = db.session.query(models.ProjectMembers).filter_by(
582 project_id=project_id,
583 user_id=user_id
584 ).first()
585
586 if not is_project_member:
587 user = db.session.query(models.Users).filter_by(id=user_id).first()
588 user_name = user.name if user else f"User ID {user_id}"
589 inconsistencies.append(f"User {user_name} (ID: {user_id}) is in session but not a project member")
590
591 return inconsistencies
592
593 except Exception as e:
594 logging.error(f"Error validating session-project consistency: {str(e)}")
595 return [f"Error during validation: {str(e)}"]
596
597def fix_session_project_inconsistencies(session_id, default_role='guest'):
598 """
599 Fix inconsistencies by adding session users as project members with default role
600 """
601 try:
602 inconsistencies = validate_session_project_consistency(session_id)
603 if not inconsistencies:
604 return True, "No inconsistencies found"
605
606 # Get the session and its project
607 session_obj = db.session.query(models.Sessions).filter_by(id=session_id).first()
608 project_id = session_obj.id_projects
609
610 fixed_count = 0
611 for inconsistency in inconsistencies:
612 if "is in session but not a project member" in inconsistency:
613 # Extract user ID from the inconsistency message
614 import re
615 match = re.search(r'User .+ \‍(ID: (\d+)\‍)', inconsistency)
616 if match:
617 user_id = int(match.group(1))
618
619 # Add user as project member
620 new_member = models.ProjectMembers(
621 project_id=project_id,
622 user_id=user_id,
623 role_type=default_role
624 )
625 db.session.add(new_member)
626 fixed_count += 1
627
628 if fixed_count > 0:
629 db.session.commit()
630 return True, f"Fixed {fixed_count} inconsistencies by adding users as project members"
631 else:
632 return False, "No inconsistencies could be automatically fixed"
633
634 except Exception as e:
635 db.session.rollback()
636 logging.error(f"Error fixing session-project inconsistencies: {str(e)}")
637 return False, f"Error fixing inconsistencies: {str(e)}"
638
639def role_project(oldproject,user_obj):
640 membership = db.session.query(models.ProjectMembers).filter_by(
641 project_id=oldproject.id, user_id=user_obj.id
642 ).first() if user_obj and oldproject else None
643 role_type = membership.role_type if membership else None
644 can_manage_members = role_type in valid_manage_members
645 can_edit_session = role_type in valid_manage_project
646 return can_edit_session, can_manage_members, role_type, membership
647
648def role_session(oldsession,user_obj):
649 membership = db.session.query(models.ProjectMembers).filter_by(
650 project_id=oldsession.id_projects, user_id=user_obj.id
651 ).first() if user_obj and oldsession else None
652 role_type = membership.role_type if membership else None
653 can_manage_members = role_type in valid_manage_members
654 can_edit_session = role_type in valid_manage_project
655 return can_edit_session, can_manage_members, role_type, membership
656
657def can_create_invite_links(user_id, project_id):
658 """
659 Check if a user can create invitation links for a project
660 Returns (can_create, reason)
661 """
662 try:
663 # Check if user is a project member
664 membership = db.session.query(models.ProjectMembers).filter_by(
665 project_id=project_id,
666 user_id=user_id
667 ).first()
668
669 if not membership:
670 return False, "User is not a project member"
671
672 # Check role permissions (restrict to owner)
673 if membership.role_type in valid_manage_members:
674 return True, f"User has {membership.role_type} role"
675 else:
676 return False, f"Role '{membership.role_type}' cannot create invitation links"
677
678 except Exception as e:
679 logging.error(f"Error checking invite permissions: {str(e)}")
680 return False, f"Error checking permissions: {str(e)}"
681
682# copy users in Session
683def copy_users_session(newsession,oldusers):
684 if ( len(oldusers) > 0 ):
685 # register only known oldusers
686 for newuser in oldusers:
687 thisuserq=db.session.query(models.Users).filter_by(name=newuser.data)
688 userexists=db.session.query(thisuserq.exists())
689 if userexists.first():
690 user=thisuserq.first()
691 # don't register a user two times
692 if (user not in newsession.users):
693 logging.debug("This user not in the list : {}. We add her.him.".format(user.name))
694 newsession.users.append(user)
695 db.session.commit()
696
697 iseditor=newuser.iseditor
698 projectsession=newsession.projects
699 projectmembers=db.session.query(models.ProjectMembers).filter_by(project_id=projectsession.id).all()
700 a_member=db.session.query(models.ProjectMembers).filter_by(project_id=projectsession.id,user_id=user.id)
701 is_already_a_member=a_member.count()
702 if (is_already_a_member == 1 and iseditor):
703 hisrole=a_member[0].role_type
704 if (not (hisrole=='owner' or hisrole=='editor')):
705 logging.debug("For project %s %d user %s's role %s will be upgrading for editor."
706 % (projectsession.name,projectsession.id,user.name,hisrole))
707 a_member[0].role_type="editor"
708 db.session.commit()
709 elif (is_already_a_member == 0):
710 if (iseditor):
711 hisrole="editor"
712 else:
713 hisrole="viewer"
714 new_member = models.ProjectMembers(
715 project_id=projectsession.id,
716 user_id=user.id,
717 role_type=hisrole
718 )
719 logging.warning("For project %s %d user %s will be added as project member with role %s"
720 % (projectsession.name,projectsession.id,user.name,hisrole))
721 db.session.add(new_member)
722 db.session.commit()
723 else:
724 errorstr="This user doesn't exists : {}. We can't add him.".format(newuser.data)
725 flash(errorstr)
726 logging.warning(errorstr)
727
728# Define new session
729def create_newsession(sessionname, description, projectid, oldusers):
730 creation_date=datetime.datetime.now()
731 newsession = models.Sessions(name=str(sessionname),
732 description=str(description),
733 id_projects=projectid,
734 creation_date=creation_date)
735
736 exist=db.session.query(models.Sessions.id).filter(models.Sessions.name.like(sessionname)).first() is not None
737
738 if (not exist):
739 lastsession=db.session.query(models.Sessions.id).order_by(models.Sessions.id.desc()).first()
740 if ( lastsession ):
741 newsession.id=lastsession.id+1
742 else:
743 newsession.id=1
744 db.session.commit()
745 copy_users_session(newsession,oldusers)
746 db.session.commit()
747
748 # Validate consistency after session creation
749 inconsistencies = validate_session_project_consistency(newsession.id)
750 if inconsistencies:
751 logging.warning(f"Found inconsistencies in newly created session {newsession.name}: {inconsistencies}")
752 # Try to fix inconsistencies automatically
753 fix_success, fix_message = fix_session_project_inconsistencies(newsession.id, default_role='viewer')
754 if fix_success:
755 logging.info(f"Fixed session inconsistencies: {fix_message}")
756 else:
757 logging.error(f"Failed to fix session inconsistencies: {fix_message}")
758 else:
759 newsession=db.session.query(models.Sessions).filter(models.Sessions.name.like(sessionname)).one()
760
761 session["sessionname"]=str(sessionname)
762
763 return newsession,exist
764
765# Define new TileSet
766def create_newtileset(tilesetname, thesession, type_of_tiles, datapath, creation_date):
767 newtileset = models.TileSets(name=tilesetname,
768 type_of_tiles = type_of_tiles,
769 Dataset_path = datapath,
770 creation_date=creation_date)
771
772 exist=db.session.query(models.TileSets.id).filter_by(name=tilesetname).scalar() is not None
773
774 if (not exist):
775 # Last TileSet id +1
776 lasttileset=db.session.query(models.TileSets.id).order_by(models.TileSets.id.desc()).first()
777 if ( lasttileset ):
778 newtileset.id=lasttileset.id+1
779 else:
780 newtileset.id=1
781 db.session.commit()
782 thesession.tile_sets.append(newtileset)
783 db.session.commit()
784 else:
785 newtileset=db.session.query(models.TileSets).filter_by(name=tilesetname).one()
786 return newtileset,exist
787
788# Convert Tile fron json file structure to database object
789def convertTile(Mynode,tilesetname,connectionbool,urlbool,datapath):
790 try:
791 url=Mynode["url"]
792 except Keyerror as e:
793 traceback.print_exc(file=sys.stderr)
794 raise(e)
795
796 ConnectionPort=0
797 if (urlbool and len(datapath) > 0):
798 # Detect if path is already in tiles source url
799 searchPath=re.search(r''+datapath,url)
800 # if no add Dataset_path in url
801 if (not searchPath):
802 url=datapath+Mynode["url"]
803 elif (connectionbool):
804 searchPort=re.search(r'port=\d+',url)
805 if (searchPort):
806 ConnectionPort=int(searchPort.group().replace('port=',''))
807 try:
808 title=Mynode["title"]
809 except :
810 title=Mynode["name"]
811 try:
812 name=Mynode["name"]
813 except:
814 name=Mynode["title"]
815
816 comment=""
817 try:
818 comment=Mynode["comment"]
819 except:
820 pass
821
822 tags=[]
823 try:
824 searchtsn=re.search(r''+tilesetname,str(Mynode["tags"]))
825 if (not searchtsn):
826 if (type(Mynode["tags"]) == 'str'):
827 tags=[tilesetname,Mynode["tags"]]
828 else:
829 tags=[tilesetname]+Mynode["tags"]
830 else :
831 tags=Mynode["tags"]
832 except:
833 #traceback.print_exc(file=sys.stderr)
834 tags=[tilesetname]
835 pass
836
837 variable=""
838 try:
839 variable=Mynode["variable"]
840 except:
841 pass
842
843 pos_px_x=-1
844 pos_px_y=-1
845 try:
846 pos_px_x=Mynode["pos_px_x"]
847 pos_px_y=Mynode["pos_px_y"]
848 except:
849 pass
850
851 IdLocation=-1
852 try:
853 IdLocation=Mynode["IdLocation"]
854 except:
855 pass
856
857 return title,name,comment,tags,variable,pos_px_x,pos_px_y,IdLocation,url,ConnectionPort
858
859# Copy a connection when copy a TileSet
860# => copy connection + config files + vnctransfert=json.loads(session["connection"+str(idconnection)]) + session["connection"+str(idconnection)]
861# TODO message to connection user owner grid : "Are you OK to copy your connection for tileset.name ?"
862def copy_connection(oldtileset,newtileset,newsessionname):
863 oldconnection=oldtileset.connections
864 user_id=get_user_id("copy_connection",session["username"])
865
866 message = '{"oldtilesetid":'+str(newtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
867
868 #TODO : session from/to right user for connection
869 if (oldconnection.id == None ):
870 return
871 else:
872 idconnection=oldconnection.id
873 if ( not "connection"+str(idconnection) in session):
874 flash("You must use createconnection button after have given job script file and configuration files or tarball.")
875 logging.error("You (user "+str(user_id)+") don't have connection information in your personal cookie for this connection : "+str(idconnection))
876 return
877
878 #TODO : vncpassword from/to right user for connection
879 if (user_id != oldconnection.id_users) :
880 flash("You can not access to this connection. You are not its owner.")
881 owner=db.session.query(models.Users).filter_by(id=oldconnection.id_users).one().name
882 you=db.session.query(models.Users).filter_by(id=user_id).one().name
883 logging.error("You (user "+you+") can not access to this connection owned by user "+owner)
884 return redirect(url_for(".edittileset",message=message))
885
886 creation_date=datetime.datetime.now()
887 newconnection = models.Connections(host_address=oldconnection.host_address,
888 auth_type=oldconnection.auth_type,
889 container=oldconnection.container,
890 scheduler=oldconnection.scheduler,
891 scheduler_file=oldconnection.scheduler_file,
892 id_users=user_id,
893 creation_date= creation_date)
894
895 lastconnection=db.session.query(models.Connections.id).order_by(models.Connections.id.desc()).first()
896 if ( lastconnection ):
897 newconnection.id=lastconnection.id+1
898 else:
899 newconnection.id=1
900 db.session.add(newconnection)
901 db.session.commit()
902
903 user_path=os.path.join("/TiledViz/TVFiles",str(user_id))
904 olddir=os.path.join(user_path,str(idconnection))
905 newdir=os.path.join(user_path,str(newconnection.id))
906 os.system("mkdir "+newdir)
907
908 config_files={}
909 for key in oldconnection.config_files:
910 filetmp=oldconnection.config_files[key]
911 newtmp=filetmp.replace(olddir,newdir)
912 os.system("cp "+filetmp+" "+newtmp)
913 config_files[key]=newtmp
914
915 newconnection.config_files=config_files
916 flag_modified(newconnection,"config_files")
917 newconnection.scheduler_file=oldconnection.scheduler_file
918 newtileset.id_connections=newconnection.id
919 db.session.commit()
920
921 vnctransfert=json.loads(session["connection"+str(idconnection)])
922 vncpassword=vnctransfert["vncpassword"]
923 session["connection"+str(newconnection.id)]=' {"callfunction":"edittileset",'+'"args":{"oldsessionname":"'+newsessionname+'","oldtilesetid":"'+str(newtileset.id)+'"}, "vncpassword":"'+vncpassword+'"}'
924
925 return
926
927# Copy a mirror connection and tileset files
928def copy_tileset_connection(tileset,tileset1,sessionname ):
929 oldconnection=tileset.connections
930 if (oldconnection):
931 copy_connection(tileset,tileset1,sessionname)
932 user_id=get_user_id("copy_tileset_connection",session["username"])
933 user_path=os.path.join("/TiledViz/TVFiles",str(user_id))
934 olddir=os.path.join(user_path,str(tileset.id_connections))
935 newdir=os.path.join(user_path,str(tileset1.id_connections))
936
937 config_files={}
938 for key in tileset.config_files:
939 filetmp=tileset.config_files[key]
940 newtmp=filetmp.replace(olddir,newdir)
941 os.system("cp "+filetmp+" "+newtmp)
942 config_files[key]=newtmp
943
944 tileset1.config_files=config_files
945 tileset1.launch_file=tileset.launch_file
946
947
948# Function to launch connection page
949def launch_connection(theTS, theConnect, myhost_address, myauth_type, mycontainer, myscheduler):
950 try:
951 logging.warning("editconnection: "
952 +str(session["username"])+" ; "
953 +str(myhost_address)+" ; "
954 +str(myauth_type)+" ; "
955 +str(mycontainer)+" ; "
956 +str(myscheduler)+" ; "
957 +str(theTS.id)+" ; "
958 +str(theConnect.id))
959 myflush()
960
961 # Wait NbTimeAlive for TVSecure to get VNC view to give connection again.
962 NbTimeAlive = 20
963 passpath="/home/connect"+str(theConnect.id)+"/vncpassword"
964 logging.warning("Go to vnc with path "+passpath)
965
966 count=0
967 while(True):
968 if (count > NbTimeAlive):
969 strerror="Connection has never been reach. Go back to TileSet."
970 logging.error(strerror)
971 flash(strerror)
972 message = '{"oldtilesetid": "'+str(theTS.id)+'"}'
973 logging.warning(strerror+" message "+message)
974 return redirect(url_for(".edittileset",message=message))
975 count=count+1
976 logging.debug("count = "+str(count))
977 # GET VNC password in
978 # security problem here if server is attacked ?
979 time.sleep(timeAlive)
980 #os.system("ls -la "+passpath)
981 if (os.path.isfile(passpath)):
982 with open(passpath,'r') as f:
983 vncpassword=re.sub(r'\n',r'',f.read())
984 f.close()
985 logging.debug("and password : "+vncpassword)
986 else:
987 logging.error("File not found in connection container "+passpath)
988 vncpassword=""
989
990 message = '{"oldtilesetid":'+str(theTS.id)+',"connectionid":'+str(theConnect.id)+',"sessionname":"'+session["sessionname"]+'"}'
991 session["connection"+str(theConnect.id)]=' {"callfunction":"edittileset",'+'"args":{"oldsessionname":"'+str(session["sessionname"])+'","oldtilesetid":"'+str(theTS.id)+'"}, "vncpassword":"'+vncpassword+'"}'
992
993 logging.warning("editconnection in session : "+str(session["connection"+str(theConnect.id)])+" message :"+str(message))
994 #TODO logging.debug
995 myflush()
996 return message
997 except Exception:
998 traceback.print_exc(file=sys.stderr)
999
1000
1001# Define new session
1002def save_session(oldsessionname, newsuffix, newdescription, alltiles):
1003 creation_date=datetime.datetime.now()
1004 oldsession=db.session.query(models.Sessions).filter(models.Sessions.name.like(oldsessionname)).one()
1005 projectid=oldsession.id_projects
1006 # TODO : max length of Session.name (=80) ?
1007 # mais parent with date may be too long
1008 #=> notion of heritage for session and tilsets in DB
1009 newsessionname=session["sessionname"]+'_'+newsuffix
1010 newsession = models.Sessions(name=newsessionname,
1011 description=newdescription,
1012 id_projects=projectid,
1013 creation_date=creation_date)
1014 lastsession=db.session.query(models.Sessions.id).order_by(models.Sessions.id.desc()).first()
1015 if ( lastsession ):
1016 newsession.id=lastsession.id+1
1017 else:
1018 newsession.id=1
1019 db.session.commit()
1020
1021 oldusers=oldsession.users
1022 for user in oldusers:
1023 # Use sync function to ensure consistency between project and session
1024 sync_success, sync_message = sync_user_to_project_and_session(
1025 user_id=user.id,
1026 project_id=newsession.id_projects,
1027 session_id=newsession.id,
1028 role_type='viewer' # Default role for users copied to sessions
1029 )
1030 if not sync_success:
1031 logging.warning(f"Failed to sync user {user.name} to copied session {newsession.name}: {sync_message}")
1032 else:
1033 logging.info(f"Successfully synced user {user.name} to copied session {newsession.name}")
1034
1035 newsession.config = oldsession.config
1036 flag_modified(newsession,"config")
1037 db.session.commit()
1038
1039 # Validate consistency after session copy
1040 inconsistencies = validate_session_project_consistency(newsession.id)
1041 if inconsistencies:
1042 logging.warning(f"Found inconsistencies in copied session {newsession.name}: {inconsistencies}")
1043 # Try to fix inconsistencies automatically
1044 fix_success, fix_message = fix_session_project_inconsistencies(newsession.id, default_role='viewer')
1045 if fix_success:
1046 logging.info(f"Fixed copied session inconsistencies: {fix_message}")
1047 else:
1048 logging.error(f"Failed to fix copied session inconsistencies: {fix_message}")
1049
1050 logging.warning("All tilesets :"+str([ ts.name for ts in oldsession.tile_sets]))
1051
1052 alltilesjson = alltiles["nodes"]
1053 for tileset in oldsession.tile_sets:
1054 # copy tilesets
1055 tilesetname=tileset.name
1056 logging.debug("copy tileset from :"+tileset.name)
1057
1058 urlbool=False
1059 connectionbool=False
1060 if (tileset.type_of_tiles == "URL"):
1061 urlbool=True
1062 elif(tileset.type_of_tiles == "CONNECTION"):
1063 # Creation of the tiles and launch connection to remote machine.
1064 connectionbool=True
1065
1066 datapath=tileset.Dataset_path
1067 tileset1,exist=create_newtileset(tilesetname+'_'+newsuffix, newsession,
1068 tileset.type_of_tiles, datapath, creation_date)
1069
1070 if (connectionbool):
1071 # Copy a mirror connection
1072 logging.error("copy config data :"+tileset.launch_file+" "+str(tileset.config_files))
1073
1074 copy_tileset_connection(tileset,tileset1,newsession.name)
1075 if (tileset1.connection):
1076 logging.error("copy id connection : "+str(tileset1.id_connections))
1077
1078 logging.error("copy config data 1 :"+tileset1.launch_file+" "+str(tileset1.config_files))
1079
1080
1081 if (not exist):
1082 try:
1083 db.session.add(tileset1)
1084 db.session.commit()
1085 except Exception:
1086 traceback.print_exc(file=sys.stderr)
1087 logging.warning("add tileset1 :"+tileset1.name)
1088
1089 for tile in tileset.tiles:
1090 try :
1091 i=next(i for i, item in enumerate(alltilesjson) if (item["title"] == tile.title))
1092 except StopIteration:
1093 i=-1
1094 logging.debug("new tile :"+tile.title+" i "+str(i))
1095 if (i > -1):
1096 Mynode=alltilesjson[i]
1097 #print("Mynode :",str(Mynode)," type ",str(type(Mynode))," type tags ",str(type(Mynode["tags"])))
1098 title,name,comment,tags,variable,pos_px_x,pos_px_y,IdLocation,url,ConnectionPort = \
1099 convertTile(Mynode,tilesetname,connectionbool,urlbool,datapath)
1100 tile.tags=tags
1101 tile.source= {"name" : name,
1102 "connection" : ConnectionPort,
1103 "url" : url,
1104 "variable": variable}
1105 flag_modified(tile,"source")
1106 tile.pos_px_x= pos_px_x
1107 tile.pos_px_y= pos_px_y
1108 tile.IdLocation=IdLocation
1109 tileset1.tiles.append(tile)
1110 db.session.commit()
1111 else:
1112 pass # do nothing if tile is not found in oldtileset ?
1113
1114 newsession.config = oldsession.config
1115 db.session.commit()
1116 return newsession
1117
1118def delelement(thistable, chosenElement, elementid):
1119 thiselement=db.session.query(thistable).filter_by(id=elementid)
1120 logging.warning("Delete this %s %d : %s" % (chosenElement,elementid,str(thiselement)))
1121 if (really_delete):
1122 thiselement.delete()
1123
1124def remove_this_session(sessionid):
1125 thissession=db.session.query(models.Sessions).filter_by(id=sessionid).scalar()
1126 # Suppress all link with any tileset
1127 for ts in thissession.tile_sets:
1128 thissession.tile_sets.remove(ts)
1129 # Suppress all link with any user
1130 logging.warning('in remove_this_session All users :'+str(thissession.users))
1131 logging.warning("in remove_this_session Link "+str(db.session.query(models.t_many_users_has_many_sessions).filter_by(id_sessions=sessionid).all()))
1132 for us in thissession.users:
1133 thissession.users.remove(us)
1134 db.session.commit()
1135 logging.warning('in remove_this_session All users after commit :'+str(thissession.users))
1136
1137 logging.warning("in remove_this_session Link after commit "+str(db.session.query(models.t_many_users_has_many_sessions).filter_by(id_sessions=sessionid).all()))
1138 for link in db.session.query(models.t_many_users_has_many_sessions).filter_by(id_sessions=sessionid).all():
1139 link.delete()
1140 delelement(models.Sessions, "session", sessionid)
1141 db.session.commit()
1142
1143
1144def remove_this_project(projectid):
1145 """
1146 Remove a project and all its associated data
1147 """
1148 try:
1149 thisproject = db.session.query(models.Projects).filter_by(id=projectid).first()
1150 if not thisproject:
1151 logging.warning(f"Project {projectid} not found for deletion")
1152 return False
1153
1154 # Suppress all sessions of this project
1155 project_sessions = db.session.query(models.Sessions).filter_by(id_projects=projectid).all()
1156 for thissession in project_sessions:
1157 remove_this_session(thissession.id)
1158
1159 # Remove all project members
1160 db.session.query(models.ProjectMembers).filter_by(project_id=projectid).delete()
1161
1162 # Remove the project itself
1163 delelement(models.Projects, "project", projectid)
1164 db.session.commit()
1165
1166 logging.info(f"Project {projectid} and all associated data removed successfully")
1167 return True
1168
1169 except Exception as e:
1170 db.session.rollback()
1171 logging.error(f"Error removing project {projectid}: {str(e)}")
1172 return False
1173
1174def remove_this_user(userid):
1175 thisuser=db.session.query(models.Users).filter_by(id=userid).one()
1176
1177 # Search connection wich have this user as owner
1178 user_connections=db.session.query(models.Connections).filter_by(id_users=userid).all()
1179 for thisconnection in user_connections:
1180 # TODO : use remove_this_connection(oldtileset,idconnection,userid) to suppress tmp files in TVFiles
1181 # Possible to recover TileSet from connection.id ?
1182 flash("Suppress element connection number %d." % (thisconnection.id))
1183 delelement(models.Connections, "connection", thisconnection.id)
1184
1185 # Search session where this user is present
1186 user_session=db.session.query(models.Sessions).filter(models.Sessions.users.any(id=userid)).all()
1187 for thissession in user_session:
1188 thissession.users.remove(thisuser)
1189
1190 # Search project wich have this user as owner
1191 # TODO : give the possibility to change owner of the project ?
1192 user_project=db.session.query(models.Projects).filter_by(id_users=userid).all()
1193 for thisproject in user_project:
1194 remove_this_project(thisproject.id)
1195
1196 delelement(models.Users, "user", userid)
1197
1198def remove_this_connection(oldtileset,idconnection,user_id):
1199 oldconnection=oldtileset.connections
1200 oldtilesetid=oldtileset.id
1201 logging.warning("Remove connection of tileset %d" % (oldtilesetid))
1202
1203 if (oldconnection.id != idconnection):
1204 logging.error("id %d to be removed is not this tileset %d id %d." % (idconnection, oldtilesetid, oldconnection.id))
1205 return
1206 # Build connection path
1207 user_path=os.path.join("/TiledViz/TVFiles",str(user_id))
1208 connectionpath=os.path.join(user_path,str(idconnection))
1209
1210 for (dirpath, dirname, filelist) in os.walk(connectionpath):
1211 for filename in filelist:
1212 strrm="rm -f "+os.path.join(dirpath,filename)
1213 os.system(strrm)
1214 logging.warning("Remove file for tileset "+oldtileset.name+" : "+os.path.join(dirpath,filename))
1215 os.rmdir(dirpath)
1216 logging.warning("Remove dir for tileset "+oldtileset.name+" : "+dirpath)
1217
1218 oldtileset.id_connections=None
1219 #oldtileset.type_of_tiles == None
1220 oldtileset.config_files=""
1221 flag_modified(oldtileset,"config_files")
1222 oldconnection.config_files=""
1223 flag_modified(oldconnection,"config_files")
1224 logging.warning("removeconnection: "
1225 +str(session["username"])+" ; "
1226 +str(oldtilesetid)+" ; "
1227 +str(idconnection))
1228 db.session.commit()
1229 myflush()
1230
1231 session["connection"+str(idconnection)]=""
1232 del(session["connection"+str(idconnection)])
1233
1234def myrender():
1235 myargs={}
1236 myargs["notlogin"]=True
1237 myargs["username"]=""
1238 if ("username" in session):
1239 myargs["username"]=session["username"]
1240 if (session["username"]!="Anonymous"):
1241 myargs["notlogin"]=False
1242
1243 User=db.session.query(models.Users).filter_by(name=session["username"]).one_or_none()
1244 if User is None:
1245 # User doesn't exist in database anymore (cookie corrupted or user deleted)
1246 # Clear the session and treat as Anonymous
1247 logging.warning(f"User '{session['username']}' in session doesn't exist in database. Clearing session.")
1248 session.pop("username", None)
1249 session.pop("is_client_active", None)
1250 myargs["username"]="Anonymous"
1251 myargs["notlogin"]=True
1252 userAdmin=False
1253 else:
1254 userAdmin=User.is_admin
1255 else:
1256 userAdmin=False
1257 else:
1258 userAdmin=False
1259 myargs["isadmin"]=userAdmin
1260 return myargs
1261
1262# ====================================================================
1263# Index/home page
1264@app.route('/', methods=['GET', 'POST']) # decorators for routes ; all these ones will lead to index
1265@app.route('/index', methods=['GET', 'POST'])
1266@app.route('/home', methods=['GET', 'POST'])
1267def index():
1268 try:
1269 #logging.warning(str(session))
1270 try:
1271 user = {"username" : session["username"] } # Test for cookie?
1272 except:
1273 user = {"username" : "Anonymous"}
1274 session["username"]="Anonymous"
1275 if (session["username"] != "Anonymous"):
1276 session["is_client_active"]=True
1277 if ( "projectname" in session ):
1278 project = session["projectname"]
1279 else:
1280 project = ""
1281 if ("sessionname" in session ):
1282 psession = session["sessionname"]
1283 else:
1284 psession=""
1285 except KeyError as e: # If session["username"] does not exist (no cookie yet)
1286 logging.error("Home error : "+e)
1287 # If the cookie is not present
1288 project = "noproject"
1289 psession = "nosession"
1290 session["username"]="Anonymous"
1291 user = {"username" : session["username"] }
1292 session["projectname"]=project
1293 session["sessionname"]=psession
1294 session["is_client_active"]=False
1295
1296 return render_template("main_template.html", **(myrender()), title="TiledViz home", user=user, project=project, session=psession)
1297
1298
1299# ====================================================================
1300# Register
1301@app.route('/register', methods=["GET", "POST"])
1302@limiter.limit(VulnerableRateLimit, methods=["POST"])
1303def register():
1304 showlogin=False
1305 showexist=False
1306 showknown=False
1307
1308 if ("username" in session):
1309 if (session["username"] == "Anonymous"):
1310 myform = BuildRegisterForm()()
1311 else:
1312 User=db.session.query(models.Users).filter_by(name=session["username"]).one()
1313 myform = BuildRegisterForm(Username=User.name,
1314 Useremail=User.mail,
1315 Usercomp=User.compagny,
1316 Usermanager=User.manager
1317 )()
1318 else:
1319 myform = BuildRegisterForm()()
1320
1321 if myform.validate_on_submit():
1322 logging.warning("Register new user.")
1323 myusername = myform.username.data
1324 # if (showlogin):
1325 # flash("Login requested for user {} in project {}, remember_me={}".format(myform.username.data, myform.projectname.data, myform.remember_me.data))
1326 # showlogin=False
1327 # return render_template("main_login.html", title="TiledViz register", form=myform)
1328 try:
1329 exists = db.session.query(models.Users.id).filter_by(name=myusername).scalar() is not None
1330 except Exception:
1331 exists=False
1332 if exists:
1333 if (showexist):
1334 flash("Known user {}, remember_me={}".format(myform.username.data, myform.remember_me.data))
1335 showexist=False
1336 return render_template("main_login.html", **(myrender()), title="TiledViz register", form=myform)
1337 logging.warning("username already exists.")
1338
1339 if (myform.newpassword.data):
1340 if ("username" in session):
1341 if (session["username"] == "Anonymous"):
1342 flash("You can't change password if you are connected as Anonymous.")
1343 logging.warning("You can't change password if you are connected as Anonymous.")
1344 return redirect("/login")
1345 else:
1346 flash("You can't change password if you are not connected : User must login !")
1347 logging.warning("You can't change password if you are not connected : User must login !")
1348 return redirect("/login")
1349
1350 logging.warning("Renew password for user {}.".format(myusername))
1351 User=db.session.query(models.Users).filter_by(name=myusername).one()
1352 hashpass, salt=tvdb.passprotected(myform.password.data)
1353 creation_date=datetime.datetime.now()
1354 User.creation_date=str(creation_date)
1355 User.salt=salt
1356 User.password=hashpass
1357 User.dateverified=str(creation_date)
1358 db.session.commit()
1359 user_id=User.id
1360 else:
1361 hashPassword,hashSalt=db.session.query(models.Users.password,models.Users.salt).filter_by(name=myusername)
1362 testP=tvdb.testpassprotected(models.Users,myusername,myform.password.data,hashPassword,hashSalt)
1363 if (testP):
1364 logging.info("Correct password !")
1365 session["username"] = myusername
1366 session["is_client_active"]=True
1367 user_id=get_user_id("login",session["username"])
1368 if (showknown):
1369 flash(Markup("Correct Login for user {} remember_me={}".format(myform.username.data, myform.remember_me.data)))
1370 showknown=False
1371 return render_template("main_login.html", **(myrender()), title="TiledViz register", form=myform)
1372 user = {"username" : session["username"] }
1373 session["is_client_active"]=True
1374 else:
1375 if (showknown):
1376 flash("You have entered an already existing username, but wrong password for user {}".format(myform.username.data))
1377 showknown=False
1378 return render_template("main_login.html", **(myrender()), title="TiledViz register", form=myform)
1379
1380 flash("This username {} already exists and passwd is incorrect : ".format(session["username"]))
1381 return render_template("main_login.html", **(myrender()),
1382 title="TiledViz register",
1383 form=myform)
1384 else:
1385 hashpass, salt=tvdb.passprotected(myform.password.data)
1386
1387 creation_date=datetime.datetime.now()
1388
1389 # Create the user first
1390 user = models.Users(name=str(myusername),
1391 creation_date=str(creation_date),
1392 mail=str(myform.email.data),
1393 compagny=str(myform.compagny.data),
1394 manager=str(myform.manager.data),
1395 salt=salt,
1396 password=hashpass,
1397 dateverified=str(creation_date),
1398 is_verified=False, # User not verified yet
1399 is_admin=False)
1400 db.session.add(user)
1401 db.session.commit()
1402 logging.warning("Commit new user.")
1403
1404 # Get real user ID
1405 user_id = user.id
1406
1407 if is2FaActivated:
1408 token = generate_verification_token(user_id)
1409 # Send verification email with correct token
1410 email_sent = send_verification_email(
1411 user_email=myform.email.data,
1412 username=myusername,
1413 token=token
1414 )
1415
1416 try:
1417 # Query all admins
1418 admin_records = db.session.query(models.Users.mail).filter_by(is_admin=True).all()
1419 admin_emails = [record.mail for record in admin_records if record.mail]
1420
1421 if admin_emails:
1422 logging.info(f"Sending registration alert to {len(admin_emails)} admins.")
1423 send_new_register_email(
1424 admin_emails=admin_emails,
1425 username=str(myusername),
1426 creation_date=str(creation_date),
1427 user_email=str(myform.email.data),
1428 user_company=str(myform.compagny.data),
1429 user_manager=str(myform.manager.data)
1430 )
1431 except Exception as e:
1432 logging.error(f"Failed to fetch admins or send admin notification: {e}")
1433
1434 if email_sent:
1435 logging.warning("Verification email sent to {}".format(myform.email.data))
1436 flash("Registration successful! Please check your email and click the verification link to activate your account.")
1437 else:
1438 # Email failed, remove the created user
1439 db.session.delete(user)
1440 db.session.commit()
1441 logging.error("Failed to send verification email to {}. User deleted.".format(myform.email.data))
1442 flash("Registration failed: Unable to send verification email. Please check your email address and try again.")
1443 return render_template("main_login.html", **(myrender()),
1444 title="TiledViz register",
1445 form=myform)
1446
1447 # Set admin if first user
1448 if (user_id == 1):
1449 user.is_admin=True
1450 user.is_verified=True # First user is auto-verified
1451 db.session.commit()
1452 logging.warning("New user is Admin and auto-verified.")
1453 # Auto-login first user
1454 session["username"] = myusername
1455 session["is_client_active"]=True
1456 else:
1457 # Don't auto-login new users - they need to verify email first
1458 logging.info("New user {} needs to verify email before login.".format(myusername))
1459 else:
1460 # Bypass verification email and auto-verify all new users when 2FA/verification is disabled
1461 user.is_verified = True
1462 if (user_id == 1):
1463 user.is_admin = True
1464 db.session.commit()
1465 logging.warning(f"New user {myusername} registered and auto-verified without email (is2FaActivated=False).")
1466 # Auto-login new user immediately
1467 session["username"] = myusername
1468 session["is_client_active"] = True
1469 flash("Registration successful! You are now logged in.")
1470 cookie_persistence = myform.remember_me.data
1471 logging.warning("[!] Cookie persistence set to %s" % (str(cookie_persistence)))
1472
1473 # TODO : suppress code below and options because is_verified is not
1474 logging.debug("Project Choice :"+myform.choice_project.data)
1475
1476 # Check if user is logged in (only first user or existing users)
1477 if "username" in session and session["username"] != "Anonymous":
1478 if(myform.choice_project.data == "create"):
1479 logging.info("Go to create new project for user "+session["username"]+".")
1480 return redirect("/project")
1481 else:
1482 if exists:
1483 return redirect("/allsessions")
1484 else:
1485 logging.info("New user "+session["username"]+" : create a new project or ask for invite_link.")
1486 flash("New user {} registred : please create a new project or ask another user for an invite_link.".format(session["username"]))
1487 if is2FaActivated:
1488 return render_template("main_login.html", **(myrender()),
1489 title="TiledViz register",
1490 form=myform)
1491 else:
1492 return redirect("/allsessions")
1493
1494 else:
1495 # New user not logged in - redirect to login
1496 flash("Please login as {} avec have clicked on received email on {}.".format(myform.username.data,myform.email.data))
1497 return redirect("/login")
1498 return render_template("main_login.html", **(myrender()), title="TiledViz register", form=myform)
1499
1500# OR Login
1501@app.route('/login', methods=["GET", "POST"])
1502@limiter.limit(VulnerableRateLimit, methods=["POST"])
1503def login():
1504 if ("username" not in session):
1505 session["username"] = "Anonymous"
1506 if ("username" in session):
1507 if (session["username"] == "Anonymous"):
1508 pass # On laisse passer, on veut afficher le formulaire de login
1509 else:
1510 User = db.session.query(models.Users).filter_by(name=session["username"]).first()
1511 if User:
1512 exists = User.id is not None
1513 logging.error(f"User already connected with username {session['username']}.")
1514
1515 if exists:
1516 # Check if user email is verified
1517 if not User.is_verified:
1518 flash("Your email address has not been verified yet. Please check your email and click the verification link.")
1519 return render_template("main_login.html", **(myrender()), title="TiledViz login", form=myform)
1520
1521 hashPassword = User.password
1522 hashSalt = User.salt
1523
1524 myform = BuildLoginForm(session)()
1525 myusername = myform.username.data if hasattr(myform, 'username') else session["username"]
1526 if (myusername != str(session["username"])):
1527 flash(f"User already connected with username {session['username']} but one tried to login as different user {myusername}.\n"+\
1528 +"Please logout before login as another user.")
1529 logging.error(f"User already connected with username {session['username']} but one tried to login as user {myusername}.")
1530 session.pop('username', None)
1531 session.pop("is_client_active")
1532 return redirect(url_for('index'))
1533
1534
1535 logging.error(f"User {session['username']} already connected.")
1536 flash("Your were already connected.")
1537 return redirect("/allsessions")
1538
1539 myform = BuildLoginForm(session)()
1540 if myform.validate_on_submit():
1541 myusername = myform.username.data
1542 mypassword = myform.password.data
1543 user = db.session.query(models.Users).filter_by(name=myusername).first()
1544 if user:
1545 # Check if user email is verified
1546 if not user.is_verified:
1547 if is2FaActivated:
1548 flash("Your email address has not been verified yet. Please check your email and click the verification link.")
1549 return render_template("main_login.html", **(myrender()), title="TiledViz login", form=myform)
1550 else:
1551 user.is_verified = True
1552 db.session.commit()
1553
1554 if tvdb.testpassprotected(models.Users, myusername, mypassword, user.password, user.salt):
1555 logging.info('Correct password.')
1556
1557 if is2FaActivated:
1558 # Send 2FA email with correct code
1559 code = generate_verification_code()
1560 email_sent = send_2FAcode_email(
1561 user_email=user.mail,
1562 username=myusername,
1563 code=code
1564 )
1565 # Create expiration datetime (UTC)
1566 expiration_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=360)
1567 codes2FA[myusername]={"code":code, "expiration_time":expiration_time}
1568
1569 strerror=f"We have sent you a security code on your mail. Please check your inbox before UTC {expiration_time}."
1570 flash(strerror)
1571 message = '{"username": "'+myusername+'", '+\
1572 '"newuser": "'+str(myform.newuser.data)+'", "choice_project": "'+myform.choice_project.data+'"}'
1573 logging.info(strerror+" message "+message)
1574 logging.warning(f"User {myusername} try to connect.")
1575 return redirect(url_for(".check2FA",message=message))
1576 else:
1577 session["username"] = myusername
1578 session["is_client_active"] = True
1579
1580 logging.warning(f"User {myusername} has loggined without 2FA.")
1581
1582 if (str(myform.newuser.data) == "True"):
1583 return redirect("/register")
1584
1585 # Check if there's a pending invite link
1586 if "pending_invite_link" in session:
1587 link = session.pop("pending_invite_link")
1588 return redirect(url_for(".handle_join_with_invite_link", link=link))
1589
1590 if (myform.choice_project.data == "create"):
1591 return redirect("/project")
1592 elif (myform.choice_project.data == "connect"):
1593 return redirect("/allsessions")
1594 else:
1595 return redirect("/allsessions")
1596
1597 else:
1598 flash("Invalid password")
1599 return redirect("/login")
1600 else:
1601 flash("Invalid username")
1602 return redirect("/login")
1603
1604 return render_template("main_login.html", **(myrender()), title="TiledViz login", form=myform)
1605
1606@app.route('/check2FA', methods=["GET", "POST"])
1607def check2FA():
1608 message = json.loads(request.args["message"])
1609 myusername=message["username"]
1610 if (myusername in codes2FA):
1611 code=codes2FA[myusername]["code"]
1612 expiration_time=codes2FA[myusername]["expiration_time"]
1613 else:
1614 flash("User didn't received a 2FA code. Please try to login again.")
1615 return redirect("/login")
1616
1617 myform = Build2FAForm(session,myusername)()
1618 if myform.validate_on_submit():
1619 if (datetime.timedelta(seconds=360) < datetime.datetime.utcnow()-expiration_time):
1620 flash("Timeout for code verification. Please try to login again.")
1621 return redirect("/login")
1622
1623 logging.info(f"code created {code} and from form {myform.code.data}")
1624 if (code == int(myform.code.data)):
1625 session["username"] = myusername
1626 session["is_client_active"] = True
1627
1628 logging.info(f"code OK message {message}")
1629 logging.warning(f"User {myusername} has loggined with 2FA code.")
1630 codes2FA.pop(myusername)
1631
1632 if (message["newuser"]=="True"): #distutils.util.strtobool(message["newuser"])):
1633 return redirect("/register")
1634
1635 # Check if there's a pending invite link
1636 if "pending_invite_link" in session:
1637 link = session.pop("pending_invite_link")
1638 return redirect(url_for(".handle_join_with_invite_link", link=link))
1639
1640 if (message["choice_project"] == "create"):
1641 return redirect("/project")
1642 elif (message["choice_project"] == "connect"):
1643 return redirect("/allsessions")
1644 else:
1645 flash("Code mismatch. Please try to login again.")
1646 return redirect("/login")
1647
1648 return render_template("main_login.html", **(myrender()), title="TiledViz login", form=myform)
1649
1650@app.route('/logout')
1651def logout():
1652 if ("username" in session):
1653 if (session["username"] == "Anonymous"):
1654 return redirect("/")
1655 else:
1656 flash("User is already not connected !")
1657 return redirect("/")
1658 # remove the username from the session if it is there
1659 session.pop('username', None)
1660 session.pop("is_client_active")
1661 return redirect(url_for('index'))
1662
1663@app.route('/test')
1664def testserver():
1665 # only test TiledViz server.
1666 return render_template("test.html")
1667
1668@app.route('/savesession', methods=['GET', 'POST'])
1669def savesession():
1670
1671 if ("username" in session):
1672 user_id=get_user_id("Savession",session["username"])
1673 else:
1674 flash("You are not connected. You must login before saving a session.")
1675 return redirect("/login")
1676
1677 if (not "is_client_active" in session):
1678 flash("You must be connected and not Anonymous to save a session.")
1679 return redirect("/login")
1680
1681 all_session={"username":session['username'],"is_client_active":session["is_client_active"],
1682 "projectname":session["projectname"],"sessionname":session["sessionname"]}
1683 # logging.error("basic all_session : "+str(all_session))
1684 for item in session:
1685 logging.info("item all_session : "+str(item))
1686 if item in all_session:
1687 pass
1688 elif (item == 'csrf_token'):
1689 pass
1690 else:
1691 all_session[item]=session[item]
1692
1693 logging.info("complete all_session : "+str(all_session))
1694
1695 #all_connections=
1696 # session["connection"+str(idconnection)])
1697 # session["connection"+str(newconnection.id)]
1698 json_all_session=json.JSONEncoder().encode(all_session)
1699
1700 if ( request.method == 'POST'):
1701 flash("Session cookie saved.")
1702 return redirect("/index")
1703 #return redirect(url_for('index'))
1704
1705 return render_template("savesession.html", **(myrender()),
1706 all_session=json_all_session)
1707
1708@app.route('/retreivesession', methods=["GET", "POST"])
1709def retreivesession():
1710
1711 if ("username" in session):
1712 user_id=get_user_id("retreivesession",session["username"])
1713 else:
1714 flash("You are not connected. You must login before retreive a session.")
1715 return redirect("/login")
1716
1717 # if ( session["sessionname"] in jsontransfert):
1718 # if ( "TheJson" in jsontransfert[session["sessionname"]]):
1719
1720 myform = BuildRetreiveSessionForm()()
1721
1722 if myform.validate_on_submit():
1723 logging.info("in tileset editor")
1724
1725 if(myform.goback.data):
1726 logging.warning("go back to home without session.")
1727 return redirect(url_for(".index"))
1728
1729 if (myform.session_file.data) :
1730 session_file = myform.session_file.data
1731 logging.warning("Read session_file :"+myform.session_file.data.filename)
1732 session_data = json.loads(json.loads(session_file.read().decode('utf-8')))
1733 logging.warning("Session_data :"+str(session_data))
1734
1735 session["username"]=session_data['username']
1736 session["is_client_active"]=session_data["is_client_active"]
1737 session["projectname"]=session_data["projectname"]
1738 session["sessionname"]=session_data["sessionname"]
1739 all_session={"username":session['username'],"is_client_active":session["is_client_active"],
1740 "projectname":session["projectname"],"sessionname":session["sessionname"]}
1741
1742 logging.info("basic all_session : "+str(all_session))
1743 for item in session_data:
1744 if item in all_session:
1745 pass
1746 else:
1747 session[item]=session_data[item]
1748
1749 flash("Session cookie restored.")
1750 return redirect("/index")
1751
1752 # if myform.editjson.data:
1753 # # jsontransfert[session["sessionname"]]={"callfunction": '{"function":"edittileset",'+'"args":{"oldtilesetid":"'+str(oldtilesetid)+'"}}',
1754 # # "TheJson":json_tiles}
1755 # pass
1756
1757 # return render_template("retreivesession.html")
1758 return render_template("main_login.html", **(myrender()), title="Retreive saved session for TiledViz", form=myform)
1759
1760
1761# Create new project
1762@app.route('/project', methods=["GET", "POST"])
1763def project():
1764 if ("username" in session):
1765 if (session["username"] == "Anonymous"):
1766 return redirect("/login")
1767 user_id=get_user_id("Project",session["username"])
1768 flash("Create new or use an old project for user {}".format(session["username"]))
1769 else:
1770 return redirect("/login")
1771
1772 # All projects for user using improved utility function
1773 printstr="{0:\xa0<"+projectl+"."+projectl+"}|\xa0{2:\xa0<"+datel+"."+datel+"}\xa0|\xa0{1:\xa0<"+descrl+"."+descrl+"}|\xa0{3:\xa0<"+descrl+"}"
1774
1775 # Get all projects where user is a member (any role)
1776 user_projects = get_user_projects(user_id)
1777
1778 myprojects=[]
1779 myprojects.append(('NoChoice',printstr.format("Project name","Description","Date and Time","All sessions")))
1780
1781 try:
1782 for project, role_type in user_projects:
1783 ListsessionsTheproject=db.session.query(models.Sessions.name).filter_by(id_projects=project.id)
1784 allsessionsname=[ asessionTheproject.name for asessionTheproject in ListsessionsTheproject ]
1785 thedate=project.creation_date.isoformat().replace("T"," ") if project.creation_date else "Unknown"
1786
1787 # Add role information to the display
1788 project_display_name = f"{project.name} ({role_type})"
1789
1790 myprojects.append((str(project.id),
1791 printstr.format(
1792 project_display_name,
1793 project.description or "",
1794 thedate,
1795 str(allsessionsname))
1796 ))
1797 except Exception as e:
1798 logging.error(f"Error loading user projects: {str(e)}")
1799 pass
1800
1801 myform = BuildNewProjectForm(myprojects)()
1802
1803 # UI filtering: adapt action choices based on selected project role
1804 try:
1805 # Determine selected project id (if any) and user's role for it
1806 selected_val = myform.chosen_project.data
1807 selected_project_id = None
1808 try:
1809 if selected_val and selected_val != "NoChoice":
1810 selected_project_id = int(selected_val)
1811 except Exception:
1812 selected_project_id = None
1813
1814 if selected_project_id:
1815 role = db.session.query(models.ProjectMembers.role_type).filter_by(
1816 project_id=selected_project_id,
1817 user_id=user_id
1818 ).scalar()
1819 can_edit_role = role in ["owner", "admin", "editor"]
1820 if not can_edit_role:
1821 myform.action_sessions.choices = [("use","Use an existing session for the grid")]
1822 myform.action_sessions.default = "use"
1823 except Exception:
1824 pass
1825 if myform.validate_on_submit():
1826 logging.info("in project")
1827
1828 if (myform.chosen_project.data=="NoChoice"):
1829 if (myform.projectname.data != ""):
1830 project_id = db.session.query(models.Projects.id).filter_by(name=myform.projectname.data).scalar()
1831 else:
1832 logging.warning("You must create a new project or choose an old one.")
1833 flash("You must create a new project or choose an old one.")
1834 return redirect("/project")
1835
1836 else:
1837 project_id = int(myform.chosen_project.data)
1838
1839 exists = project_id is not None
1840 logging.debug("Project exists "+str(exists)+" id : "+str(project_id))
1841
1842 if exists:
1843 # Check if user has access to this project
1844 if not can_access_project(project_id, user_id):
1845 flash("You don't have permission to access this project!")
1846 return redirect("/project")
1847
1848 if (myform.chosen_project.data == "NoChoice"):
1849 session["projectname"]=myform.projectname.data
1850 else:
1851 session["projectname"]=db.session.query(models.Projects.name).filter_by(id=project_id).scalar()
1852
1853 oldproject=db.session.query(models.Projects).filter_by(id=project_id).scalar()
1854 # Permission: only owner may manage members (add users)
1855 try:
1856 current_user_obj = db.session.query(models.Users).filter_by(name=session["username"]).first()
1857 can_edit_session, can_manage_members, role_type, membership = role_project(oldproject,current_user_obj)
1858 except Exception:
1859 can_manage_members = False
1860 can_edit_session = False
1861
1862 logging.debug("Chosen project : "+str(session["projectname"]))
1863 # Route behavior depends on role: only editors+ can create/modify
1864 user_can_edit = can_manage_project(project_id, user_id)
1865 if(myform.action_sessions.data == "create"):
1866 if (can_edit_session):
1867 logging.warning("Create new session ")
1868 flash("Create new session for user {}".format(session["username"]))
1869 return redirect("/newsession")
1870 else:
1871 sentence="You don't have permission to create new session on this project!"
1872 logging.warning(sentence)
1873 flash(sentence)
1874 return redirect("/project")
1875 else:
1876 logging.debug("Use old sessions (view)")
1877 flash("Use an old session for user {}".format(session["username"]))
1878 return redirect("/oldsessions")
1879 elif (myform.chosen_project.data=="NoChoice"):
1880 creation_date=datetime.datetime.now()
1881 screation_date=str(creation_date)
1882 logging.error("create project date "+screation_date)
1883
1884 try:
1885 project = models.Projects(name=str(myform.projectname.data),
1886 creation_date=screation_date,
1887 id_users=user_id,
1888 role_type="owner",
1889 description=myform.description.data)
1890 db.session.add(project)
1891 db.session.flush() # Get the project ID
1892
1893 # Create the owner membership record using utility function
1894 success, message = add_project_member(project.id, user_id, 'owner')
1895 if not success:
1896 db.session.rollback()
1897 flash(f"Error creating project: {message}")
1898 return redirect("/project")
1899
1900 session["projectname"]=myform.projectname.data
1901 logging.debug("Project created : create new session ")
1902 return redirect("/newsession")
1903
1904 except Exception as e:
1905 db.session.rollback()
1906 logging.error(f"Error creating project: {str(e)}")
1907 flash(f"Error creating project: {str(e)}")
1908 return redirect("/project")
1909 else:
1910 logging.error("Error for chosen project.")
1911 return redirect("/project")
1912
1913 return render_template("main_login.html", **(myrender()), title="New project TiledViz", form=myform)
1914
1915# List all my old projects and after all sessions I am in
1916@app.route('/admin', methods=["GET", "POST"])
1917def admin():
1918 if ("username" in session):
1919 if (session["username"] == "Anonymous"):
1920 return redirect("/login")
1921 user_id=get_user_id("admin",session["username"])
1922 logging.warning("Admin page with user {}".format(session["username"]))
1923 if (really_delete):
1924 flash("Admin page with user {}\nALERT : click any suppress buttons will remove elements in DB.".format(session["username"]))
1925 else:
1926 flash("Admin page with user {}".format(session["username"]))
1927 logging.warning("User id {}".format(user_id))
1928 else:
1929 flash("Admin page : User must login !")
1930 return redirect("/login")
1931
1932 User=db.session.query(models.Users).filter_by(name=session["username"]).one()
1933 userAdmin=User.is_admin
1934 if not userAdmin:
1935 flash("You must be an administrator to access this page.")
1936 return redirect("/")
1937
1938 message='{"username": '+session["username"]+'}'
1939 logging.info("in administration page.")
1940
1941 if (userAdmin):
1942 allusers = db.session.query(models.Users).all()
1943 logging.debug("All users :"+str([ theuser.name for theuser in allusers]))
1944
1945 printstr="{0:\xa0<"+usernamel+"."+usernamel+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0|\xa0{2:\xa0<"+descrl+"."+descrl+"}"
1946 listallusers=[]
1947 listallusers.append(('NoChoice',printstr.format("User name","Date and Time","Description")))
1948
1949 for thisuser in allusers:
1950 thedate=thisuser.creation_date.isoformat().replace("T"," ")
1951 if (thisuser.mail):
1952 mail=thisuser.mail
1953 else:
1954 mail=""
1955 if (thisuser.compagny):
1956 compagny=thisuser.compagny
1957 else:
1958 compagny=""
1959 if (thisuser.manager):
1960 manager=thisuser.manager
1961 else:
1962 manager=""
1963 Desc=mail+"; "+compagny+"; "+manager
1964 Desc=Desc[:int(descrl)]
1965 listallusers.append(
1966 (str(thisuser.id),printstr.
1967 format(str(thisuser.name),
1968 thedate,Desc)
1969 )
1970 )
1971
1972 # Handle admin status changes
1973 if request.method == 'POST':
1974 if 'toggle_admin' in request.form:
1975 user_id = request.form.get('user_id')
1976 try:
1977 user = db.session.query(models.Users).filter_by(id=user_id).one()
1978 user.is_admin = not user.is_admin
1979 db.session.commit()
1980 flash(f"Admin status updated for user {user.name}")
1981 except Exception as e:
1982 flash(f"Error updating admin status: {str(e)}")
1983 return redirect(url_for('admin'))
1984
1985 allprojects = db.session.query(models.Projects).all()
1986 logging.debug("All projects :"+str([ theproject.name for theproject in allprojects]))
1987
1988 printstr="{0:\xa0<"+projectl+"."+projectl+"}|\xa0{1:\xa0<"+usernamel+"."+usernamel+"}\xa0|\xa0{2:\xa0<"+datel+"."+datel+"}\xa0|\xa0{3:\xa0<"+descrl+"."+descrl+"}"
1989 listallprojects=[]
1990 listallprojects.append(('NoChoice',printstr.format("Project name","Owner","Date and Time","Description")))
1991
1992 for thisproject in allprojects:
1993 thedate=thisproject.creation_date.isoformat().replace("T"," ")
1994 Desc=thisproject.description
1995
1996 # Get owner via project_members
1997 owner_member = db.session.query(models.ProjectMembers).filter(
1998 models.ProjectMembers.project_id == thisproject.id,
1999 models.ProjectMembers.role_type == 'owner'
2000 ).first()
2001
2002 owner_name = 'Unknown'
2003 if owner_member and owner_member.user:
2004 owner_name = owner_member.user.name
2005 elif thisproject.id_users: # Fallback on old relation
2006 old_owner = db.session.query(models.Users).filter_by(id=thisproject.id_users).first()
2007 if old_owner:
2008 owner_name = old_owner.name
2009
2010 listallprojects.append(
2011 (str(thisproject.id),printstr.
2012 format(str(thisproject.name), owner_name, thedate, Desc)
2013 )
2014 )
2015
2016
2017 allsessions = db.session.query(models.Sessions).all()
2018 logging.debug("All sessions :"+str([ thesession.name for thesession in allsessions]))
2019
2020 printstr="{0:\xa0<"+sessionl+"."+sessionl+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0|\xa0{2:\xa0<"+descrl+"."+descrl+"}"
2021 listallsessions=[]
2022 listallsessions.append(('NoChoice',printstr.format("Session name","Date and Time","Description")))
2023
2024 for thissession in allsessions:
2025 thedate=thissession.creation_date.isoformat().replace("T"," ")
2026 Desc=thissession.description
2027 listallsessions.append(
2028 (str(thissession.id),printstr.
2029 format(str(thissession.name),
2030 thedate,Desc)
2031 )
2032 )
2033
2034 projects = db.session.query(models.Projects).filter_by(id_users=user_id).all()
2035 logging.debug("My projects :"+str([ theproject.name for theproject in projects]))
2036
2037 printstr="{0:\xa0<"+projectl+"."+projectl+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0|\xa0{2:\xa0<"+descrl+"."+descrl+"}"
2038 listmyprojects=[]
2039 listmyprojects.append(('NoChoice',printstr.format("Project name","Date and Time","Description")))
2040
2041 for thisproject in projects:
2042 thedate=thisproject.creation_date.isoformat().replace("T"," ")
2043 Desc=thisproject.description
2044 listmyprojects.append(
2045 (str(thisproject.id),printstr.
2046 format(str(thisproject.name),
2047 thedate,Desc)
2048 )
2049 )
2050
2051 # All sessions own of those projects
2052 mysessions=[]
2053 try:
2054 for theproject in projects:
2055 ListsessionsTheproject=db.session.query(models.Sessions.name).filter_by(id_projects=theproject.id).all()
2056 [ mysessions.append((theproject.name,ListsessionTheproject)) for ListsessionTheproject in ListsessionsTheproject ]
2057 except:
2058 pass
2059 logging.debug("My sessions :"+str(mysessions))
2060
2061 printstr="{1:\xa0<"+sessionl+"."+sessionl+"}|{0:\xa0<"+projectl+"."+projectl+"}|\xa0{2:\xa0<"+datel+"."+datel+"}\xa0|\xa0{3:\xa0<"+descrl+"."+descrl+"}"
2062 listmyprojectssession=[]
2063 listmyprojectssession.append(('NoChoice',printstr.format("Project name","Session name","Date and Time","Description")))
2064 listmysession=[]
2065 for thissessions in mysessions:
2066
2067 for thissession in thissessions[1]:
2068 listmysession.append(thissession)
2069 thedate="1970-01-01"
2070 try:
2071 thedate=db.session.query(models.Sessions.creation_date).filter_by(name=str(thissession)).scalar().isoformat().replace("T"," ")
2072 except:
2073 pass
2074 SessDesc=db.session.query(models.Sessions).filter_by(name=thissession).scalar().description
2075 thissessionid=db.session.query(models.Sessions.id).filter_by(name=str(thissession)).one()
2076 listmyprojectssession.append(
2077 (str(thissessionid),printstr.
2078 format(str(thissessions[0]),
2079 str(thissession),
2080 thedate,SessDesc)
2081 )
2082 )
2083
2084
2085 # # All sessions this user has been invited to
2086 # listsessions=[]
2087
2088 # invite_sessions = db.session.query(models.Sessions.name).filter(models.Sessions.users.any(id=user_id)).all()
2089 # printstr="{0:\xa0<"+sessionl+"."+sessionl+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0|\xa0{2:\xa0<"+descrl+"."+descrl+"}"
2090 # listsessions.append(('NoChoice',printstr.format("Session name","Date and Time","Description")))
2091 # for thissession in invite_sessions:
2092 # logging.debug("Build listsessions for invite_session "+str(thissession.name))
2093 # if (thissession.name not in listmysession):
2094 # thedate="1970-01-01"
2095 # try:
2096 # thedate=db.session.query(models.Sessions.creation_date).filter_by(name=thissession.name).scalar().isoformat().replace("T"," ")
2097 # except:
2098 # pass
2099 # SessDesc=db.session.query(models.Sessions).filter_by(name=thissession.name).scalar().description
2100 # listsessions.append(
2101 # (str(thissession.name),printstr.
2102 # format(str(thissession.name),
2103 # thedate, SessDesc)
2104 # )
2105 # )
2106
2107
2108 # list all my Connections
2109 connections = db.session.query(models.Connections).filter_by(id_users=user_id)
2110 logging.debug("My connections :"+str([ theconnection.host_address for theconnection in connections]))
2111
2112 printstr="{0:\xa0<"+connectionh+"."+connectionh+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0\xa0\xa0\xa0\xa0|\xa0{2:\xa0<"+datel+"."+datel+"}\xa0\xa0\xa0\xa0|\xa0{3:\xa0<"+datel+"."+datel+"}"
2113 listmyconnections=[]
2114 listmyconnections.append(('NoChoice',printstr.format("Host address","Date and Time","id","In session")))
2115 for thisconnection in connections:
2116 insession=False
2117 if ("connection"+str(thisconnection.id) in session):
2118 insession=True
2119 listmyconnections.append(
2120 (str(thisconnection.id),printstr.
2121 format(str(thisconnection.host_address),
2122 str(thisconnection.creation_date),
2123 str(thisconnection.id),
2124 str(insession))
2125 )
2126 )
2127
2128 if (userAdmin):
2129 listallconnections=[]
2130 # list all Connections
2131 allconnections = db.session.query(models.Connections)
2132 logging.debug("All connections :"+str([ theconnection.host_address for theconnection in allconnections]))
2133
2134 printstr="{0:\xa0<"+usernamel+"."+usernamel+"}|\xa0"+\
2135 "{1:\xa0<"+connectionh+"."+connectionh+"}|\xa0"+\
2136 "{2:\xa0<"+datel+"."+datel+"}\xa0\xa0\xa0\xa0\xa0|\xa0"+\
2137 "{3:\xa0<"+datel+"."+datel+"}\xa0\xa0\xa0\xa0|\xa0"+\
2138 "{4:\xa0<"+datel+"."+datel+"}"
2139 listallconnections.append(('NoChoice',printstr.format("Username","Host address","Date and Time","id","In session")))
2140 for thisconnection in allconnections:
2141 insession=False
2142 if ("connection"+str(thisconnection.id) in session):
2143 insession=True
2144 theuser=db.session.query(models.Users).filter_by(id=thisconnection.id_users).one().name
2145 listallconnections.append(
2146 (str(thisconnection.id),printstr.
2147 format(str(theuser),
2148 str(thisconnection.host_address),
2149 str(thisconnection.creation_date),
2150 str(thisconnection.id),
2151 str(insession))
2152 )
2153 )
2154
2155 logging.debug("My project sessions :"+str(listmyprojectssession))
2156 # logging.debug("My invited sessions :"+str(listmysession))
2157 if (userAdmin):
2158 myform = BuildAdminForm(listmyprojects,listmyprojectssession,listmyconnections,
2159 list_all_users=listallusers,list_all_projects=listallprojects,list_all_sessions=listallsessions,list_all_connections=listallconnections)()
2160 else:
2161 myform = BuildAdminForm(listmyprojects,listmyprojectssession,listmyconnections)()
2162 #,listsessions
2163 if myform.validate_on_submit():
2164 objects=[]
2165 ids=[]
2166
2167 # TODO : GOT TO A NEW PAGE with the list of confirmations of deletations
2168
2169 if (userAdmin):
2170 if (myform.suprressfreetiles.data):
2171 flash("All free tiles were suppressed.")
2172 for freetile in db.session.query(models.t_freetiles).all():
2173 delelement(models.Tiles, "tile", freetile[0])
2174
2175 if (myform.suprressUnusedTilesets.data):
2176 flash("All free tilesets were suppressed.\n You may suppress all free tiles now.")
2177 for freetilesets in db.session.query(models.t_freetilesets).all():
2178 elementid=freetilesets[0]
2179 logging.warning("Delete this %s %d : %s" % ("tileset",elementid,str(freetilesets[1])))
2180 db.session.query(func.deltileset(freetilesets[0])).all()
2181 #delelement(models.TileSets, "tileset", freetilesets[0])
2182
2183 if ( myform.suppressSelected.data):
2184 if (userAdmin):
2185 if (myform.all_users.data != "NoChoice"):
2186 chosenObject="user"
2187 elementid=int(myform.all_users.data)
2188 logging.warning("Admin suppress %s %d " % (chosenObject,elementid))
2189 flash("Admin suppress element %s number %s." % (chosenObject,elementid))
2190 objects.append(chosenObject)
2191 ids.append(elementid)
2192
2193 remove_this_user(elementid)
2194
2195 if (myform.all_projects.data != "NoChoice"):
2196 chosenObject="project"
2197 elementid=int(myform.all_projects.data)
2198 logging.warning("Admin suppress %s %d " % (chosenObject,elementid))
2199 flash("Admin suppress element %s number %s." % (chosenObject,elementid))
2200 objects.append(chosenObject)
2201 ids.append(elementid)
2202
2203 remove_this_project(elementid)
2204
2205 if (myform.all_sessions.data != "NoChoice"):
2206 chosenObject="session"
2207 elementid=int(myform.all_sessions.data)
2208 logging.warning("Admin suppress %s %d " % (chosenObject,elementid))
2209 flash("Suppress element %s number %s." % (chosenObject,elementid))
2210 objects.append(chosenObject)
2211 ids.append(elementid)
2212
2213 remove_this_session(elementid)
2214
2215
2216
2217 if (myform.chosen_project.data != "NoChoice"):
2218 chosenObject="project"
2219 elementid=int(re.sub(r'[(),]','',myform.chosen_project.data))
2220 logging.warning("Chosen my %s %d " % (chosenObject,elementid))
2221 flash("Suppress element %s number %d." % (chosenObject,elementid))
2222 objects.append(chosenObject)
2223 ids.append(elementid)
2224
2225 delelement(models.Projects, chosenObject, elementid)
2226
2227 if (myform.chosen_project_session.data != "NoChoice"):
2228 chosenObject="mysession"
2229 elementid=int(re.sub(r'[(),]','',myform.chosen_project_session.data))
2230 logging.warning("Chosen my %s %d " % (chosenObject,elementid))
2231 flash("Suppress element %s number %d." % (chosenObject,elementid))
2232 objects.append(chosenObject)
2233 ids.append(elementid)
2234
2235 thissession=db.session.query(models.Sessions).filter_by(id=elementid).scalar()
2236 remove_this_session(elementid)
2237
2238 # if (myform.chosen_session_invited.data != "NoChoice"):
2239 # logging.warning("Chosen session invited "+str(myform.chosen_session_invited.data))
2240 # chosenObject="invitedsession"
2241 # elementid=myform.chosen_session_invited.data
2242 # flash("Suppress element %s number %s." % (chosenObject,elementid))
2243
2244 if (myform.chosen_user_connection.data != "NoChoice"):
2245 chosenObject="connection"
2246 elementid=int(myform.chosen_user_connection.data)
2247 logging.warning("Chosen my %s %d " % (chosenObject,elementid))
2248 flash("Suppress element %s number %d." % (chosenObject,elementid))
2249 objects.append(chosenObject)
2250 ids.append(elementid)
2251
2252 try:
2253 thisConnection=db.session.query(models.Connections).filter_by(id=elementid).one()
2254 user_id=thisConnection.id_users
2255 oldtileset=db.session.query(models.TileSets).filter_by(id_connections=elementid).one()
2256 if (oldtileset.connections.id==elementid):
2257 remove_this_connection(oldtileset,elementid,user_id)
2258 else:
2259 delelement(models.Connections, chosenObject, elementid)
2260 except:
2261 delelement(models.Connections, chosenObject, elementid)
2262
2263 if (myform.suppressAllMyConnections.data):
2264 flash("All my connections were suppressed {}".format(session["username"]))
2265
2266 for thisconnection in connections:
2267 chosenObject="connection"
2268 objects.append(chosenObject)
2269 ids.append(thisconnection.id)
2270 try:
2271 user_id=thisconnection.id_users
2272 oldtileset=db.session.query(models.TileSets).filter_by(id_connections=thisconnection.id).one()
2273 if (oldtileset.connections.id==thisconnection.id):
2274 remove_this_connection(oldtileset,thisconnection.id,user_id)
2275 else:
2276 delelement(models.Connections, chosenObject, thisconnection.id)
2277 except:
2278 delelement(models.Connections, chosenObject, thisconnection.id)
2279
2280
2281 if (userAdmin):
2282
2283 if (myform.chosen_connections.data != "NoChoice"):
2284 chosenObject="connection"
2285 elementid=int(myform.chosen_connections.data)
2286 logging.warning("Chosen my %s %d " % (chosenObject,elementid))
2287 flash("Suppress element %s number %d." % (chosenObject,elementid))
2288 objects.append(chosenObject)
2289 ids.append(elementid)
2290
2291 try:
2292 thisConnection=db.session.query(models.Connections).filter_by(id=elementid).one()
2293 user_id=thisConnection.id_users
2294 oldtileset=db.session.query(models.TileSets).filter_by(id_connections=elementid).one()
2295 if (oldtileset.connections.id==elementid):
2296 remove_this_connection(oldtileset,elementid,user_id)
2297 else:
2298 delelement(models.Connections, chosenObject, elementid)
2299 except:
2300 delelement(models.Connections, chosenObject, elementid)
2301
2302
2303 if (myform.suppressAllConnections.data):
2304 flash("All connections for all users were suppressed.")
2305 # TODO : use remove_this_connection(oldtileset,idconnection,user_id) to suppress tmp files in TVFiles
2306 # Possible to recover TileSet from connection.id ?
2307 for thisconnection in allconnections:
2308 chosenObject="connection"
2309 objects.append(chosenObject)
2310 ids.append(thisconnection.id)
2311 try:
2312 user_id=thisconnection.id_users
2313 oldtileset=db.session.query(models.TileSets).filter_by(id_connections=thisconnection.id).one()
2314 if (oldtileset.connections.id==thisconnection.id):
2315 remove_this_connection(oldtileset,thisconnection.id,user_id)
2316 else:
2317 delelement(models.Connections, chosenObject, thisconnection.id)
2318 except:
2319 delelement(models.Connections, chosenObject, thisconnection.id)
2320
2321
2322 db.session.commit()
2323
2324 return redirect("/admin")
2325
2326 return render_template("main_login.html", **(myrender()), title="Admin for user.", form=myform, message=message)
2327
2328
2329# List all my old projects and after all sessions I am in
2330@app.route('/allsessions', methods=["GET", "POST"])
2331def allmysessions():
2332 if ("username" in session):
2333 if (session["username"] == "Anonymous"):
2334 return redirect("/login")
2335
2336 flash("All projects and sessions for user {}".format(session["username"]))
2337 logging.warning("All projects and sessions for user {}".format(session["username"]))
2338 user_id=get_user_id("allsavessions",session["username"])
2339 logging.warning("User id {}".format(user_id))
2340 else:
2341 flash("All projects and sessions : User must login !")
2342 return redirect("/login")
2343
2344 message='{"username": '+session["username"]+'}'
2345 logging.info("in allsessions")
2346
2347 # All projects own by user
2348 projects = db.session.query(models.Projects).filter_by(id_users=user_id)
2349 logging.debug("My projects :"+str([ theproject.name for theproject in projects]))
2350
2351 # All sessions own of those projects
2352 mysessions=[]
2353 try:
2354 for theproject in projects:
2355 ListsessionsTheproject=db.session.query(models.Sessions.name).filter_by(id_projects=theproject.id)
2356 [ mysessions.append((theproject.name,ListsessionTheproject)) for ListsessionTheproject in ListsessionsTheproject ]
2357 except:
2358 pass
2359 logging.debug("My sessions :"+str(mysessions))
2360
2361 printstr="{1:\xa0<"+sessionl+"."+sessionl+"}|{0:\xa0<"+projectl+"."+projectl+"}|\xa0{2:\xa0<"+datel+"."+datel+"}\xa0|\xa0{3:\xa0<"+descrl+"."+descrl+"}"
2362 listmyprojectssession=[]
2363 listmyprojectssession.append(('NoChoice',printstr.format("Project name","Session name","Date and Time","Description")))
2364 listmysession=[]
2365 for thissessions in mysessions:
2366 for thissession in thissessions[1]:
2367 listmysession.append(thissession)
2368 dbSession=db.session.query(models.Sessions).filter(models.Sessions.name.like(thissession)).first()
2369 logging.debug(f"Session : {thissession} {dbSession}")
2370 try:
2371 logging.debug(f" : {dbSession.name}")
2372 thedate=dbSession.creation_date.isoformat().replace("T"," ")
2373 except:
2374 logging.debug(f"Error date session {thissession}")
2375 thedate="1972-08-05"
2376 pass
2377 try:
2378 SessDesc=dbSession.description
2379 except:
2380 logging.debug(f"Error desc session {thissession}")
2381 SessDesc="Unknown"
2382 pass
2383
2384 listmyprojectssession.append(
2385 (str(thissession),printstr.
2386 format(str(thissessions[0]),
2387 str(thissession),
2388 thedate,SessDesc)
2389 )
2390 )
2391
2392 # All sessions this user has been invited to
2393 listsessions=[]
2394
2395 invite_sessions = db.session.query(models.Sessions.name).filter(models.Sessions.users.any(id=user_id)).all()
2396 printstr="{0:\xa0<"+sessionl+"."+sessionl+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0|\xa0{2:\xa0<"+descrl+"."+descrl+"}"
2397 listsessions.append(('NoChoice',printstr.format("Session name","Date and Time","Description")))
2398 for thissession in invite_sessions:
2399 if (thissession.name not in listmysession):
2400 dbSession=db.session.query(models.Sessions).filter(models.Sessions.name.like(thissession.name)).first()
2401 logging.debug(f"iSession : {thissession} {dbSession}")
2402 try:
2403 logging.debug(f" : {dbSession.name}")
2404 thedate=dbSession.creation_date.isoformat().replace("T"," ")
2405 except:
2406 logging.debug(f"Error date invited session {thissession}")
2407 thedate="1972-08-05"
2408 pass
2409 try:
2410 SessDesc=dbSession.description
2411 except:
2412 logging.debug(f"Error desc invited session {thissession}")
2413 SessDesc="Unknown"
2414 pass
2415 listsessions.append(
2416 (str(thissession.name),printstr.
2417 format(str(thissession.name),
2418 thedate, SessDesc)
2419 )
2420 )
2421
2422 logging.debug("My project sessions :"+str(listmyprojectssession))
2423 logging.debug("My invited sessions :"+str(listsessions))
2424 if (len(listmyprojectssession) == 0 and len(listmysession) == 0 and len(invite_sessions) == 0):
2425 flash("You are not in any project or session now. Please create your first one or ask somebody to invite you.")
2426 return redirect(url_for(".index"))
2427
2428 myform = BuildAllProjectSessionForm(listmyprojectssession,listsessions)()
2429
2430 # UI filtering: hide edit button if user has no edit rights on any project
2431 try:
2432 has_edit_rights = db.session.query(models.ProjectMembers).filter(
2433 models.ProjectMembers.user_id == user_id,
2434 models.ProjectMembers.role_type.in_(["owner","admin","editor"])
2435 ).count() > 0
2436 if not has_edit_rights and hasattr(myform, 'edit'):
2437 try:
2438 myform.edit.render_kw = {"style": "display:none"}
2439 except Exception:
2440 pass
2441 except Exception:
2442 pass
2443 if myform.validate_on_submit():
2444 if (myform.chosen_project_session.data != "NoChoice"):
2445 logging.debug("Chosen project session "+str(myform.chosen_project_session.data))
2446 session["sessionname"]=myform.chosen_project_session.data
2447 elif (myform.chosen_session_invited.data != "NoChoice"):
2448 logging.debug("Chosen session invited "+str(myform.chosen_session_invited.data))
2449 session["sessionname"]=myform.chosen_session_invited.data
2450 else:
2451 logging.warning("You must choose a session")
2452 flash("You didn't select a session or click 'Go' button on search bar.\n"+
2453 "You must choose a session in your projects or one you were invited on.")
2454 return redirect("/allsessions")
2455
2456 logging.warning("Which is session "+str(db.session.query(models.Sessions.id).filter(models.Sessions.name.like(f'{session["sessionname"]}')).first()))
2457 its_project_id=db.session.query(models.Sessions).filter(models.Sessions.name.like(f'{session["sessionname"]}')).first().id_projects
2458 session["projectname"]=db.session.query(models.Projects).filter_by(id=its_project_id).scalar().name
2459 logging.warning("And have project id "+str(its_project_id)+" which is "+str(session["projectname"]))
2460 session["is_client_active"]=True
2461 if(myform.edit.data):
2462 # Back-end guard: only owner/admin/editor may access edit view
2463 if not can_manage_project(its_project_id, user_id):
2464 flash("You don't have permission to edit sessions in this project.")
2465 return redirect("/grid")
2466 logging.debug("go to edit old session : "+session["sessionname"])
2467 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
2468 return redirect(url_for(".editsession",message=message))
2469 return redirect("/grid")
2470
2471 return render_template("main_login.html", **(myrender()), title="All projects/sessions TiledViz", form=myform, message=message)
2472
2473
2474# List all old sessions for the projectname I am in
2475@app.route('/oldsessions', methods=["GET", "POST"])
2476def oldsessions():
2477 if ("username" in session):
2478 if (session["username"] == "Anonymous"):
2479 return redirect("/login")
2480 else:
2481 flash("Old sessions : User must login !")
2482 return redirect("/login")
2483# Old Session page
2484 if (not "projectname" in session):
2485 flash("Old sessions : Must define a project first !")
2486 return redirect("/project")
2487
2488 # Validate user access to the project and restrict actions for viewer/guest
2489 try:
2490 Project = db.session.query(models.Projects).filter_by(name=session["projectname"]).first()
2491 user_id=get_user_id("OldSessionsPerm",session["username"])
2492 if not Project or not can_access_project(Project.id, user_id):
2493 flash("You don't have permission to access this project.")
2494 return redirect("/project")
2495 user_can_edit = can_manage_project(Project.id, user_id)
2496 except Exception:
2497 user_can_edit = False
2498
2499 Project = db.session.query(models.Projects).filter_by(name=session["projectname"]).scalar()
2500 project_id=Project.id
2501 project_desc=Project.description
2502 querysessions = db.session.query(models.Sessions.name).filter_by(id_projects=project_id)
2503 listsessions=[]
2504 for thissession in querysessions:
2505 logging.debug("Build listsessions "+str(thissession[0]))
2506 listsessions.append((str(thissession[0]),str(thissession[0])))
2507 oldproject={"name":session["projectname"],
2508 "description":project_desc}
2509 logging.debug("Old project : "+str(oldproject["name"])+" list old sessions :"+str(listsessions))
2510
2511 # Permission: only owner may manage members (add users)
2512 try:
2513 current_user_obj = db.session.query(models.Users).filter_by(name=session["username"]).first()
2514 can_edit_session, can_manage_members, role_type, membership = role_project(Project,current_user_obj)
2515 except Exception:
2516 can_manage_members = False
2517 can_edit_session = False
2518
2519 myform = BuildOldProjectForm(oldproject, listsessions, session)()
2520 if myform.validate_on_submit():
2521 session["sessionname"]=myform.chosen_session.data
2522 if(myform.from_session.data=="use"):
2523 logging.debug("reuse old session : "+myform.chosen_session.data)
2524 session["is_client_active"]=True
2525 return redirect("/grid")
2526 elif(myform.from_session.data=="edit"):
2527 if not user_can_edit:
2528 flash("You don't have permission to edit sessions in this project.")
2529 return redirect("/oldsessions")
2530 logging.debug("go to edit old session : "+myform.chosen_session.data)
2531 message = '{"oldsessionname":"'+myform.chosen_session.data+'"}'
2532 return redirect(url_for(".editsession",message=message))
2533 elif(myform.from_session.data=="copy"):
2534 if not user_can_edit:
2535 flash("You don't have permission to copy sessions in this project.")
2536 return redirect("/oldsessions")
2537 logging.debug("go to copy old session : "+myform.chosen_session.data)
2538 message = '{"oldsessionname":"'+myform.chosen_session.data+'"}'
2539 return redirect(url_for(".copysession",message=message))
2540
2541 return render_template("main_login.html", **(myrender()), title="Old projects TiledViz", form=myform)
2542
2543# ==> invite an existing user (and connected) with invite_link
2544# Create new session
2545@app.route('/newsession', methods=["GET", "POST"])
2546def newsession():
2547 if ("username" in session):
2548 if (session["username"] == "Anonymous"):
2549 return redirect("/login")
2550 else:
2551 flash("Create new sessions : User must login !")
2552 return redirect("/login")
2553
2554 # Permission guard: only owner/admin/editor may create a session in the current project
2555 try:
2556 user_id=get_user_id("NewSessionPerm",session["username"])
2557 Project = db.session.query(models.Projects).filter_by(name=session.get("projectname")).first()
2558 if not Project or not can_manage_project(Project.id, user_id):
2559 flash("You don't have permission to create sessions for this project.")
2560 return redirect("/project")
2561 except Exception:
2562 flash("Unable to validate permissions for creating a session.")
2563 return redirect("/project")
2564
2565 # New or session manager (copy, invite_link a list of connected users ?)
2566 myform = BuildNewSessionForm()()
2567 if myform.validate_on_submit():
2568 if myform.add_users.data:
2569 myform.users.append_entry()
2570 flash("New user for user {} in session {}".format(session["username"], myform.sessionname.data))
2571 return render_template("main_login.html", **(myrender()), title="New session TiledViz", form=myform)
2572 logging.info("in session")
2573 id_projects=db.session.query(models.Projects.id).filter_by(name=session["projectname"])
2574 sessionname=myform.sessionname.data
2575
2576 oldusers=[]
2577 if ("users" in myform):
2578 list_users=[]
2579 i=0
2580 logging.debug("myform.users : %s" % str(myform.users))
2581 for newuser in myform.users:
2582 soup=BeautifulSoup(str(newuser),'lxml')
2583 outsoup=soup.find_all("input")
2584 outfind=[ input.get("value") for input in outsoup ]
2585 outchecked=[ input.get("checked") for input in outsoup ]
2586 if (len(outfind[0]) > 0):
2587 logging.debug("myform.users : %s %s" % (str(outfind[0]),str(outchecked[1] == "")))
2588 thisuser=FormUser(data=outfind[0],iseditor=(outchecked[1]==""))
2589 list_users.append(thisuser)
2590
2591 ok_list_users="Owner %s add users %s to the session %s" % (session["username"],str(list_users),sessionname)
2592 logging.debug("ok_list_users : %s " % ok_list_users)
2593 oldusers=list_users
2594
2595 newsession,exist=create_newsession(sessionname, myform.description.data, id_projects, oldusers)
2596 if (not exist):
2597 try:
2598 db.session.add(newsession)
2599 db.session.commit()
2600 except Exception:
2601 traceback.print_exc(file=sys.stderr)
2602
2603 flash("Session with name {} creation problem.".format(sessionname))
2604 return render_template("main_login.html", **(myrender()), title="New Session TiledViz", form=myform, message=message)
2605 else:
2606 flash("Session with name {} already exists".format(sessionname))
2607 return render_template("main_login.html", **(myrender()), title="New Session TiledViz", form=myform)
2608
2609 # Create default config for new session
2610 config_default_file=open("app/static/js/config_default.json",'r')
2611 json_configs=json.load(config_default_file)
2612 config_default_file.close()
2613 newsession.config=json_configs
2614 db.session.commit()
2615
2616 if myform.Session_config.data:
2617 message = '{"sessionname":"'+newsession.name+'"}'
2618 return redirect(url_for(".configsession",message=message))
2619 else:
2620 #message='{"username":"'+session["username"]+'","sessionname":"'+session["sessionname"]+'"}'
2621 #return redirect(url_for(".addtileset",message=message))
2622 flash("One must validate new session before create a tiledset.")
2623 message='{"oldsessionname":"'+session["sessionname"]+'"}'
2624 return redirect(url_for(".editsession",message=message))
2625 return render_template("main_login.html", **(myrender()), title="New session TiledViz", form=myform)
2626
2627
2628# Copy an old session and edit tilesets
2629@app.route('/copysession', methods=["GET", "POST"])
2630def copysession():
2631 if ("username" in session):
2632 if (session["username"] == "Anonymous"):
2633 return redirect("/login")
2634 else:
2635 flash("Copy session : User must login !")
2636 return redirect("/login")
2637
2638 if ( not "sessionname" in session ):
2639 return redirect("/allsessions")
2640
2641 message=json.loads(request.args["message"])
2642 oldsessionname=message["oldsessionname"]
2643 oldsession = db.session.query(models.Sessions).filter(models.Sessions.name.like(oldsessionname)).first()
2644 # Permission: only owner may manage members (add users)
2645 try:
2646 current_user_obj = db.session.query(models.Users).filter_by(name=session["username"]).first()
2647 can_edit_session, can_manage_members, role_type, membership = role_session(oldsession,current_user_obj)
2648 except Exception:
2649 can_manage_members = False
2650 can_edit_session = False
2651 role_type="viewer"
2652
2653 if (can_edit_session):
2654 flash("User {} don't have rights to copy session {} with role {}".format(session["username"], myform.sessionname.data, role_type))
2655 return redirect("/oldsessions")
2656
2657 session["can_manage_members"]=can_manage_members
2658 session["can_edit_session"]=can_edit_session
2659 myform = BuildEditsessionform(oldsession,session,edit=False)()
2660 if myform.validate_on_submit():
2661 logging.debug("copySessionForm : ")
2662 # TODO : multiple
2663 if myform.add_users.data:
2664 myform.users.append_entry()
2665 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
2666 flash("New user avaible for user {} in session {}".format(session["username"], myform.sessionname.data))
2667 # TODO !! => send invitation to new users ?
2668 return render_template("main_login.html", **(myrender()), title="Copy session TiledViz", form=myform)
2669
2670 list_users=[]
2671 i=0
2672 for newuser in myform.users:
2673 soup=BeautifulSoup(str(newuser),'lxml')
2674 outsoup=soup.find_all("input")
2675 outfind=[ input.get("value") for input in outsoup ]
2676 outchecked=[ input.get("checked") for input in outsoup ]
2677 if (len(outfind) > 0):
2678 logging.debug("myform.users : %s %s" % (str(outfind[0]),str(outchecked[1] == "")))
2679 thisuser=FormUser(data=outfind[0],iseditor=(outchecked[1]==""))
2680 list_users.append(thisuser)
2681
2682 newsession,exist=create_newsession(myform.sessionname.data, myform.description.data, oldsession.id_projects, list_users)
2683
2684 if (not exist):
2685 nbts=len(oldsession.tile_sets)
2686 for i in range(nbts):
2687 newsession.tile_sets.append(oldsession.tile_sets[i])
2688
2689 try:
2690 # if user has not change session.name, it can't be created.
2691 db.session.add(newsession)
2692 db.session.commit()
2693
2694 # Validate consistency after session copy
2695 inconsistencies = validate_session_project_consistency(newsession.id)
2696 if inconsistencies:
2697 logging.warning(f"Found inconsistencies in copied session {newsession.name}: {inconsistencies}")
2698 # Try to fix inconsistencies automatically
2699 fix_success, fix_message = fix_session_project_inconsistencies(newsession.id, default_role='viewer')
2700 if fix_success:
2701 logging.info(f"Fixed copied session inconsistencies: {fix_message}")
2702 else:
2703 logging.error(f"Failed to fix copied session inconsistencies: {fix_message}")
2704
2705 except Exception:
2706 traceback.print_exc(file=sys.stderr)
2707
2708 message = '{"oldsessionname":"'+oldsessionname+'"}'
2709 flash("Session with name {} creation problem.".format(newsession.name))
2710 return render_template("main_login.html", **(myrender()), title="Copy session TiledViz", form=myform, message=message)
2711 else:
2712 try:
2713 flash("You must change session name {} for new session.".format(newsession.data))
2714 except:
2715 flash("You must change session name {} for new session.".format(str(newsession)))
2716 return render_template("main_login.html", **(myrender()), title="Copy Session TiledViz", form=myform)
2717
2718 theaction=myform.tilesetaction.data
2719
2720 # try:
2721 # oldtilesetid=int(myform.tilesetchoice.data)
2722 # message = '{"oldtilesetid":'+str(tilesetid)+'}'
2723 # except :
2724 # traceback.print_exc(file=sys.stderr)
2725 # if ( theaction != "search" and theaction != "copy" ): #and theaction != "useold"
2726 # logging.error("You must check a tileset for this action {}".format(theaction))
2727 # flash("You must check a tileset for this action {}".format(theaction))
2728 # message = '{"oldsessionname":'+newsession.name+'}'
2729 # return redirect(url_for(".editsession",message=message))
2730
2731 logging.debug("Action tileset : "+str(message)+" "+theaction)
2732 if(theaction == "useold"):
2733 session["is_client_active"]=True
2734 return redirect("/grid")
2735 elif myform.Session_config.data:
2736 message = '{"sessionname":"'+newsession.name+'"}'
2737 return redirect(url_for(".configsession",message=message))
2738 elif(theaction == "copy"):
2739 tilesetid=int(myform.tilesetchoice.data)
2740 message = '{"oldtilesetid":"'+str(tilesetid)+'"}'
2741 return redirect(url_for(".copytileset",message=message))
2742 elif (theaction == "search"):
2743 message = '{"oldsessionname":"'+newsession.name+'"}'
2744 return redirect(url_for(".searchtileset",message=message))
2745 return render_template("main_login.html", **(myrender()), title="Copy session TiledViz", form=myform, message=message)
2746
2747
2748# Edit old session
2749@app.route('/editsession', methods=["GET", "POST"])
2750def editsession():
2751 if ("username" in session):
2752 if (session["username"] == "Anonymous"):
2753 return redirect("/login")
2754 else:
2755 flash("Edit session : User must login !")
2756 return redirect("/login")
2757
2758 message=json.loads(request.args["message"])
2759 oldsessionname=message["oldsessionname"]
2760 oldsession = db.session.query(models.Sessions).filter(models.Sessions.name.like(oldsessionname)).first()
2761 # Permission: only owner may manage members (add users)
2762 try:
2763 current_user_obj = db.session.query(models.Users).filter_by(name=session["username"]).first()
2764 can_edit_session, can_manage_members, role_type, membership = role_session(oldsession,current_user_obj)
2765 except Exception:
2766 can_manage_members = False
2767 can_edit_session = False
2768
2769 session["can_manage_members"]=can_manage_members
2770 session["can_edit_session"]=can_edit_session
2771
2772 myform = BuildEditsessionform(oldsession,session,edit=True)()
2773 if myform.validate_on_submit():
2774 # Redirect to project member management (owner/admin only)
2775 if hasattr(myform, 'manage_members') and myform.manage_members.data:
2776 if not can_manage_members:
2777 message = '{"oldsessionname":"'+oldsessionname+'"}'
2778 flash("Only owners or admins can manage project members.")
2779 return render_template("main_login.html", **(myrender()), title="Edit session TiledViz", form=myform, can_manage_members=can_manage_members)
2780 return redirect(url_for('.project_members', project_id=oldsession.id_projects))
2781 logging.debug("editSessionForm : ")
2782
2783 if (myform.editusers.data):
2784 return redirect(url_for('project_members', project_id=oldsession.projects.id))
2785
2786 # - PCA -
2787 # |_ my form contains the form the edition tileset
2788 # |_ process the anatreada script here ??
2789 # logging.info("Calling pca_nodes_of_tilesets methode of anatread")
2790 # logging.info("The user as selected the option : " + myform.has_pca.data)
2791
2792 # - PCA -
2793 # if has_pca option is True -> make the PCA over all the tisets of the current session
2794 if myform.has_pca.data == "YES" :
2795
2796 # logging.info("Contruction of nodes of tilesets to process the PCA ...")
2797
2798 # - PCA -
2799 # |_ structure of dict_tiles_all_session :
2800 #
2801 # {
2802 # "nodes": [
2803 # {
2804 # "id": "xxxxxxxxxx",
2805 # "title": "xxxxxxxxxx",
2806 # "tags": [
2807 # "{tagName,minValue,value,maxValue}",
2808 # "{tagName2,minValue,value,maxValue}",
2809 # "tagName3"
2810 # ]
2811 # }
2812 # ]
2813 # }
2814
2815 dict_tiles_all_session = dict()
2816 dict_tiles_all_session["nodes"] = []
2817 # ici
2818 for tileset in oldsession.tile_sets:
2819 for tile in tileset.tiles:
2820 dict_tiles_all_session["nodes"].append( {
2821 "id" : tile.id,
2822 "title" : tile.title,
2823 "tags" : tile.tags
2824 } )
2825
2826 # dict_pca_tiles_all_session : contains a json of dict_tiles_all_session with the group for all the tiles
2827 # groups_dict : contains a json of a dict as this format : {id_tile : group_name}
2828 json_tiles_text=json.dumps(dict_tiles_all_session)
2829 logging.error(json_tiles_text)
2830 myflush()
2831 dict_pca_tiles_all_session, groups_dict = anatreada.pca_on_multiple_nodes(json_tiles_text)
2832
2833 dict_pca_tiles_all_session = json.loads(dict_pca_tiles_all_session)
2834 groups_dict = json.loads(groups_dict)
2835
2836 # Update oldession with the new groups
2837 for i in range(0, len(oldsession.tile_sets)):
2838 tileset = oldsession.tile_sets[i]
2839 for j in range(0, len(tileset.tiles)):
2840 try :
2841 tile = tileset.tiles[j]
2842 tile.tags.append(groups_dict[str(tile.id)])
2843
2844 flag_modified(tile,"tags")
2845
2846 except :
2847 logging.error("An error occurred : can't find tile id in groups dict ...")
2848
2849 db.session.commit()
2850
2851 if ("add_users" in myform):
2852 if myform.add_users.data:
2853 myform.users.append_entry()
2854 message = '{"oldsessionname":"'+oldsessionname+'"}'
2855 flash("New user avaible for user {} in session {}".format(session["username"], myform.sessionname.data))
2856 # TODO !! => send invitation to new users ? => just a mail to give the notif
2857 return render_template("main_login.html", **(myrender()), title="Edit session TiledViz", form=myform)
2858
2859 if (myform.sessionname.data != oldsessionname):
2860 message = '{"oldsessionname":"'+oldsessionname+'"}'
2861 flash("You must NOT change session name to edit session {}".format(oldsessionname))
2862 return render_template("main_login.html", **(myrender()), title="Edit session TiledViz", form=myform, message=message)
2863
2864 if can_edit_session:
2865 oldsession.description=str(myform.description.data)
2866 creation_date=datetime.datetime.now()
2867 oldsession.creation_date=str(creation_date)
2868 db.session.commit()
2869 session["sessionname"]=oldsessionname
2870
2871 if ("users" in myform):
2872 list_users=[]
2873 i=0
2874 logging.debug("myform.users : %s" % str(myform.users))
2875 for newuser in myform.users:
2876 soup=BeautifulSoup(str(newuser),'lxml')
2877 outsoup=soup.find_all("input")
2878 outfind=[ input.get("value") for input in outsoup ]
2879 outchecked=[ input.get("checked") for input in outsoup ]
2880 if (len(outfind[0]) > 0):
2881 logging.debug("myform.users : %s %s" % (str(outfind[0]),str(outchecked[1] == "")))
2882 thisuser=FormUser(data=outfind[0],iseditor=(outchecked[1]==""))
2883 list_users.append(thisuser)
2884
2885 ok_list_users="Owner %s add users %s to the session %s" % (session["username"],str(list_users),oldsessionname)
2886 logging.debug("ok_list_users : %s " % ok_list_users)
2887
2888 copy_users_session(oldsession,list_users)
2889 db.session.commit()
2890
2891 if myform.Session_config.data and can_edit_session:
2892 message = '{"sessionname":"'+oldsessionname+'"}'
2893 return redirect(url_for(".configsession",message=message))
2894
2895 theaction=myform.tilesetaction.data
2896
2897 ListAllTileSet_ThisSession=[ (str(thistileset.id), thistileset.name) for thistileset in oldsession.tile_sets]
2898 if (len(ListAllTileSet_ThisSession) > 0):
2899 try:
2900 tilesetid=int(myform.tilesetchoice.data)
2901 message = '{"oldtilesetid":'+str(tilesetid)+'}'
2902 except Exception:
2903 #traceback.print_exc(file=sys.stderr)
2904 if ( theaction not in ["search","copy","create","useold"] and not (hasattr(myform,'edit') and myform.edit.data)):
2905 logging.debug("You must check a tileset for this action {}".format(theaction))
2906 flash("You must check a tileset for this action {}".format(theaction))
2907 message = '{"oldsessionname":"'+oldsessionname+'"}'
2908 return redirect(url_for(".editsession",message=message))
2909 if(hasattr(myform,'edit') and myform.edit.data and can_edit_session):
2910 logging.debug("message before edittileset "+str(message))
2911 return redirect(url_for(".edittileset",message=message))
2912
2913 logging.debug("Action tileset : "+str(message)+" "+theaction)
2914 if(theaction == "useold"):
2915 session["is_client_active"]=True
2916 return redirect("/grid")
2917 elif (theaction == "create") and can_edit_session:
2918 flash("Tileset requested for user {} in session {}".format(session["username"],session["sessionname"]))
2919 message='{"username":"'+session["username"]+'","sessionname":"'+session["sessionname"]+'"}'
2920 return redirect(url_for(".addtileset",message=message))
2921 elif(theaction == "copy") and can_edit_session:
2922 return redirect(url_for(".copytileset",message=message))
2923 elif(theaction == "search") and can_edit_session:
2924 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
2925 return redirect(url_for(".searchtileset",message=message))
2926 elif(theaction == "remove") and can_edit_session:
2927 try:
2928 thistileset=db.session.query(models.TileSets).filter_by(id=tilesetid).scalar()
2929 logging.debug("TileSet for remove : "+str(thistileset))
2930 oldsession.tile_sets.remove(thistileset)
2931 db.session.commit()
2932 except Exception:
2933 traceback.print_exc(file=sys.stderr)
2934
2935 flash("Error remove tileset {}".format(db.session.query(models.TileSets).filter_by(id=tilesetid).scalar().name))
2936 message = '{"oldsessionname":"'+oldsessionname+'"}'
2937 return redirect(url_for(".editsession",message=message))
2938 else:
2939 # For view-only users trying non-authorized actions
2940 if not can_edit_session:
2941 flash("You have view-only permissions on this project.")
2942 return render_template("main_login.html", **(myrender()), title="Edit session TiledViz", form=myform, message='{"oldsessionname":"'+oldsessionname+'"}')
2943 return render_template("main_login.html", **(myrender()), title="Edit session TiledViz", form=myform, message=message)
2944
2945# Use json editor for session nodes.json
2946@app.route('/editnodes', methods=["GET", "POST"])
2947def editnodes():
2948 if ("username" in session):
2949 if (session["username"] == "Anonymous"):
2950 return redirect("/login")
2951 else:
2952 flash("Edit nodes : User must login !")
2953 return redirect("/login")
2954
2955 message=json.loads(request.args["message"])
2956 oldsessionname=message["oldsessionname"]
2957 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name.like(oldsessionnam)).first()
2958
2959 # Permissions: only owner/editor can edit nodes
2960 try:
2961 current_user_obj = db.session.query(models.Users).filter_by(name=session["username"]).first()
2962 membership = db.session.query(models.ProjectMembers).filter_by(
2963 project_id=ThisSession.id_projects, user_id=current_user_obj.id
2964 ).first() if current_user_obj and ThisSession else None
2965 role_type = membership.role_type if membership else None
2966 can_edit_nodes = role_type in valid_manage_project
2967 except Exception:
2968 can_edit_nodes = False
2969
2970 if not can_edit_nodes:
2971 flash("You do not have permission to edit nodes in this project.")
2972 return redirect("/grid")
2973
2974 if (type(ThisSession) != type(None)):
2975 ListAllTileSet_ThisSession=ThisSession.tile_sets
2976 else:
2977 logging.warning("You must choose a valid session for editnodes")
2978 flash("You didn't select a valid session in editnodes.")
2979 return redirect("/editsession")
2980
2981 session["tilesetnames"]=[ thistileset.name for thistileset in ListAllTileSet_ThisSession ]
2982 logging.debug("All TileSet for session "+str(session["sessionname"])+" : "+str(session["tilesetnames"]))
2983 # Main loop to build the grid :
2984 nbr_of_tiles=0
2985
2986 # build all tiles data vector
2987 global tiles_data
2988 tiles_data={}
2989 tiles_data["nodes"]=[]
2990 ts=0
2991 lts=len(ThisSession.tile_sets)
2992 while (ts < lts):
2993 thistileset=ThisSession.tile_sets[ts]
2994 nbtiles=len(thistileset.tiles)
2995 nbr_of_tiles = nbr_of_tiles + nbtiles
2996
2997 if (thistileset.type_of_tiles == "CONNECTION"):
2998 oldconnection=thistileset.connections
2999 out_nodes_json = os.path.join("/TiledViz/TVFiles", str(oldconnection.id_users), str(oldconnection.id),"nodes.json")
3000 tiledata=[]
3001 if ( os.path.exists( out_nodes_json ) ):
3002 try:
3003 with open(out_nodes_json) as json_tiles_file:
3004 tsjson=json.loads(json_tiles_file.read())
3005
3006 logging.warning("nodes.json read and OK "+out_nodes_json)
3007 tiledata=tsjson["nodes"]
3008 for O in tsjson:
3009 if ( O != "nodes" ):
3010 if ( O in tiles_data ):
3011 tiles_data[O]=tiles_data[O]+tsjson[O]
3012 else:
3013 tiles_data[O]=tsjson[O]
3014 except Exception as err:
3015 traceback.print_exc(file=sys.stderr)
3016 strerror="Error from json "+out_nodes_json+" file from connection : "+str(err)
3017 logging.error(strerror)
3018
3019 else:
3020 tiledata=tvdb.encode_tileset(thistileset)
3021
3022 tiles_data["nodes"]=tiles_data["nodes"]+tiledata
3023 ts=ts+1
3024 jsontransfert[session["sessionname"]]={"callfunction": '{"function":"show_grid",'+'"args":{"sessionname":"'+oldsessionname+'"}}'}
3025 jsontransfert[session["sessionname"]]["TheJson"]=tiles_data
3026
3027 return redirect(url_for(".jsoneditor"))
3028
3029
3030# List all old tilesets I am in
3031@app.route('/searchtileset', methods=["GET", "POST"])
3032def searchtileset():
3033# Old Tileset page
3034 if ("username" in session):
3035 if (session["username"] == "Anonymous"):
3036 return redirect("/login")
3037 else:
3038 flash("Search TileSet : User must login !")
3039 return redirect("/login")
3040 try:
3041 message=json.loads(request.args["message"])
3042 except json.decoder.JSONDecodeError as e:
3043 logging.error("message error ! "+str(e))
3044 logging.error("message : "+str(request.args["message"]))
3045 traceback.print_exc(file=sys.stderr)
3046 message=json.loads(request.args["message"].replace("'", '"'))
3047
3048 oldsessionname=message["oldsessionname"]
3049 oldsession = db.session.query(models.Sessions).filter(models.Sessions.name.like(oldsessionname)).first()
3050
3051
3052 # Guard: edit permission required on owning project to add tilesets from search
3053 try:
3054 user_id=get_user_id("SearchTileSetPerm",session["username"])
3055 if not oldsession or not can_manage_project(oldsession.id_projects, user_id):
3056 flash("You don't have permission to modify tilesets for this project.")
3057 return redirect("/allsessions")
3058 except Exception:
3059 flash("Unable to validate permissions for tileset search.")
3060 return redirect("/allsessions")
3061
3062 querysessions= db.session.query(models.Sessions).filter(models.Sessions.users.any(name=session["username"])).all()
3063
3064 printstr="{0:\xa0<"+tilesetl+"."+tilesetl+"}|\xa0{1:\xa0<"+datel+"."+datel+"}\xa0|\xa0{2:\xa0<"+descrl+"."+descrl+"}"
3065
3066 listtilesets=[]
3067 listtilesets.append(('NoChoice',printstr.format("Tileset name","Date and Time","Data Path")))
3068 for thissession in querysessions:
3069 #thissession=db.session.query(models.Sessions).filter_by(id=thissessionid[0])
3070 for tileset in thissession.tile_sets:
3071 if ( tileset.name not in listtilesets ):
3072 thedate=db.session.query(models.TileSets.creation_date).filter_by(name=tileset.name).scalar().isoformat().replace("T"," ")
3073 logging.warning("Compare thedate : %s %s" % (thedate, tileset.creation_date.isoformat().replace("T"," ")))
3074 listtilesets.append((str(tileset.id),
3075 printstr.format(
3076 str(tileset.name),
3077 thedate,
3078 tileset.Dataset_path)))
3079 # logging.warning("For user : "+session["username"]+" list old tilesets :"+str(listtilesets).replace("\xa0"," ").replace("('","\n('"))
3080
3081 myform = BuildOldTileSetForm(session["username"], listtilesets)()
3082 if myform.validate_on_submit():
3083 if (myform.chosen_tileset.data=="NoChoice"):
3084 flash("Error : no TileSet Selected.")
3085 return redirect(url_for(".searchtileset",message=message))
3086 else:
3087 logging.warning("Out of forms, add tilesets :"+str(myform.chosen_tileset.data))
3088 thisTS=db.session.query(models.TileSets).filter_by(id=myform.chosen_tileset.data).scalar()
3089 logging.warning("For user : "+session["username"]+", add tilesets :"+str(thisTS.name))
3090 oldsession.tile_sets.append(thisTS)
3091 db.session.commit()
3092 message = '{"oldsessionname":"'+oldsessionname+'"}'
3093 flash("Add tileSet {}.".format(thisTS.name))
3094 return redirect(url_for(".editsession",message=message))
3095
3096 return render_template("main_login.html", **(myrender()), title="All my Tiledsets ", form=myform)
3097
3098# Config Session : give the json (depend of static/js/config_default.json)
3099@app.route('/configsession', methods=["GET", "POST"])
3100def configsession():
3101 if ("username" in session):
3102 if (session["username"] == "Anonymous"):
3103 return redirect("/login")
3104 else:
3105 flash("Config session : User must login !")
3106 return redirect("/login")
3107 if ( not "sessionname" in session ):
3108 return redirect("/allsessions")
3109
3110 message=json.loads(request.args["message"])
3111
3112 sessionname=message["sessionname"]
3113 thesession = db.session.query(models.Sessions).filter(models.Sessions.name.like(sessionname)).first()
3114
3115 # Detect how the data of config has been inserted :
3116 if ( session["sessionname"] in jsontransfert):
3117 if ( "TheJson" in jsontransfert[session["sessionname"]]):
3118 # if TheJson is already define in message, it has been edited by jsoneditor (call beside)
3119 json_configs=jsontransfert[session["sessionname"]]["TheJson"]
3120 jsontransfert[session["sessionname"]].pop("TheJson")
3121 logging.debug("configsession : come back from jsoneditor")
3122 # json_gziped=message["TheJson"].replace("b'","").replace("'","")
3123 # json_unziped=gzip.decompress(base64.b64decode(json_gziped)).decode('utf-8')
3124 # #print("configsession json_unziped", json_unziped)
3125 # json_configs=json.loads(json_unziped)
3126 #print("configsession json_configs", json_configs)
3127 else:
3128 logging.debug("configsession : we can't be here !")
3129 if ( thesession.config == None ):
3130 logging.debug("configsession : using config_default.json")
3131 config_default_file=open("app/static/js/config_default.json",'r')
3132 json_configs=json.load(config_default_file)
3133 config_default_file.close()
3134 else:
3135 logging.debug("configsession : old session config.")
3136 json_configs=thesession.config
3137 else:
3138 if ( thesession.config == None ):
3139 logging.debug("configsession : using config_default.json")
3140 config_default_file=open("app/static/js/config_default.json",'r')
3141 json_configs=json.load(config_default_file)
3142 config_default_file.close()
3143 else:
3144 logging.debug("configsession : old session config.")
3145 json_configs=thesession.config
3146
3147 json_configs_text=json.JSONEncoder().encode(json_configs)
3148 #logging.debug("json_configs_text : "+json_configs_text)
3149 myform = BuildConfigSessionForm(json_configs,json_configs_text)()
3150
3151 if myform.validate_on_submit():
3152 # if json structure is inserted with text area
3153 json_configs = myform.json_config_text.data
3154
3155 if myform.editjson.data:
3156 # json_gziped=base64.b64encode(gzip.compress(json_configs.encode('utf-8')))
3157 # callfunction = '{"function":"configsession",'+'"args":{"sessionname":"'+sessionname+'"}}'
3158 jsontransfert[session["sessionname"]]={"callfunction": '{"function":"configsession",'+'"args":{"sessionname":"'+sessionname+'"}}',
3159 "TheJson":json_configs}
3160 # return redirect(url_for(".jsoneditor",callfunction=callfunction,TheJson=json_gziped))
3161 return redirect(url_for(".jsoneditor"))
3162
3163 # Translate json text in structure
3164 jsonConfigs = json.loads(json_configs)
3165 #logging.debug("json_configs modify : "+str(jsonConfigs))
3166
3167 # json_configs_file = FileField("File json object for tileset ")
3168 # json_file = open(json_file_name).read()
3169
3170 thesession.config=jsonConfigs
3171 flag_modified(thesession,"config")
3172 db.session.commit()
3173
3174 message = '{"oldsessionname":"'+sessionname+'"}'
3175 return redirect(url_for(".editsession",message=message))
3176
3177 return render_template("main_login.html", **(myrender()), title="Config session", form=myform, message=message)
3178
3179
3180# New TileSet : always create tile even if another (title/comment) exists
3181@app.route('/addtileset', methods=["GET", "POST"])
3182def addtileset():
3183 if ("username" in session):
3184 if (session["username"] == "Anonymous"):
3185 return redirect("/login")
3186 else:
3187 flash("Add TileSet : User must login !")
3188 return redirect("/login")
3189 if ( not "sessionname" in session ):
3190 return redirect("/allsessions")
3191
3192 # Guard: user must be allowed to edit the project owning the session
3193 try:
3194 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
3195 user_id=get_user_id("AddTileSetPerm",session["username"])
3196 if not ThisSession or not can_manage_project(ThisSession.id_projects, user_id):
3197 flash("You don't have permission to add tilesets in this project.")
3198 return redirect("/allsessions")
3199 except Exception:
3200 flash("Unable to validate permissions for tileset creation.")
3201 return redirect("/allsessions")
3202
3203 myform = BuildTilesSetForm()()
3204 #print('message=',str(request.args["message"]))
3205 message=json.loads(request.args["message"])
3206 if myform.validate_on_submit():
3207 logging.info("in addtileset")
3208
3209 json_tiles=None;
3210 # Detect how the data of tiles has been inserted :
3211 if (myform.json_tiles_file.data) :
3212 json_tiles_file = myform.json_tiles_file.data
3213 json_tiles = json_tiles_file.read()
3214 elif (myform.json_tiles_text.data):
3215 # if json structure is inserted with text area
3216 json_tiles = myform.json_tiles_text.data
3217
3218 if (json_tiles):
3219 # Translate json text in structure
3220 try:
3221 jsonTileSet = json.loads(json_tiles)
3222 except json.decoder.JSONDecodeError as e:
3223 logging.error("Addtileset : json TileSet error ! "+str(e))
3224 logging.error("Data : "+str(json_tiles))
3225 traceback.print_exc(file=sys.stderr)
3226 json_tiles=json.loads(json_tiles.replace("'", '"'))
3227 try:
3228 jsonTileSet = json.loads(json_tiles)
3229 except json.decoder.JSONDecodeError as e:
3230 logging.error("Addtileset : correction with double quotes not efficient ! "+str(e))
3231 flash("Please look on data for TileSet json compliance")
3232 return redirect(url_for(".addtileset",message=message))
3233
3234 nbr_of_tiles = len(jsonTileSet["nodes"])
3235 logging.info("Number of tiles "+str(nbr_of_tiles))
3236
3237 # openports_between_tiles = FieldList(IntegerField("port :",validators=[Optional()]),description="Open port in visualisation network",min_entries=2,max_entries=5)
3238
3239 urlbool=False
3240 connectionbool=False
3241 if (myform.type_of_tiles.data == "URL"):
3242 urlbool=True
3243 elif(myform.type_of_tiles.data == "CONNECTION"):
3244 launch_file=myform.script_launch_file.data
3245 if (not launch_file):
3246 flash("TileSet with connection must have at least a script to launch your tiles.\n You must add launch_file script.")
3247 return render_template("main_login.html", **(myrender()), title="New TileSet TiledViz", form=myform, message=message)
3248 connectionbool=True
3249
3250 #print(session["sessionname"])
3251 conn_session=db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
3252 creation_date=datetime.datetime.now()
3253 tilesetname=myform.name.data
3254 if ( myform.dataset_path.data ):
3255 datapath=str(myform.dataset_path.data)
3256 else:
3257 if (urlbool):
3258 datapath="https"
3259 else:
3260 datapath=""
3261
3262 newtileset,exist=create_newtileset(tilesetname, conn_session, myform.type_of_tiles.data, datapath, creation_date)
3263 if (not exist):
3264 try:
3265 db.session.add(newtileset)
3266 db.session.commit()
3267 except Exception:
3268 traceback.print_exc(file=sys.stderr)
3269
3270 flash("TileSet with name {} creation problem :".format(tilesetname))
3271 return render_template("main_login.html", **(myrender()), title="New TileSet TiledViz", form=myform, message=message)
3272 else:
3273 flash("TileSet with name {} already exists : Please give another name ".format(tilesetname))
3274 return render_template("main_login.html", **(myrender()), title="New TileSet TiledViz", form=myform, message=message)
3275
3276 # Register config files in jsontransfert to write them in connection path
3277 if (connectionbool):
3278 TStmpName="tileset_"+str(newtileset.id)
3279 if ( not TStmpName in jsontransfert):
3280 jsontransfert[TStmpName]={}
3281 if (myform.configfiles.data):
3282 for FileS in myform.configfiles.data:
3283 jsontransfert[TStmpName][FileS.filename]=FileS.read()
3284
3285 # Python file to launch case
3286 jsontransfert["tileset_"+str(newtileset.id)][launch_file.filename]=launch_file.read()
3287 newtileset.launch_file=launch_file.filename
3288 db.session.commit()
3289
3290 if (json_tiles):
3291 # Insert tiles into DB :
3292 tiles=[]
3293
3294 for i in range(nbr_of_tiles):
3295 Mynode=jsonTileSet["nodes"][i]
3296 try:
3297 title,name,comment,tags,variable,pos_px_x,pos_px_y,IdLocation,url,ConnectionPort = \
3298 convertTile(Mynode,tilesetname,connectionbool,urlbool,datapath)
3299 except Exception as e:
3300 etype, value, tb = sys.exc_info()
3301 flash("TileSet with name {} creation problem :".format(tilesetname)+"\n"+
3302 ''.join(traceback.format_tb(tb)))
3303 logging.error(''.join(traceback.format_tb(tb)))
3304 return redirect(url_for(".addtileset",message=message))
3305
3306 newtile = models.Tiles(title=title,
3307 comment=comment,
3308 tags=tags,
3309 source= {"name" : name,
3310 "connection" : ConnectionPort,
3311 "url" : url,
3312 "variable": variable
3313 },
3314 pos_px_x= pos_px_x,
3315 pos_px_y= pos_px_y,
3316 IdLocation=IdLocation,
3317 creation_date= creation_date)
3318 lasttile=db.session.query(models.Tiles.id).order_by(models.Tiles.id.desc()).first()
3319 if (lasttile):
3320 newtile.id=lasttile.id+1
3321 else:
3322 newtile.id=1
3323
3324 db.session.add(newtile)
3325 db.session.commit()
3326 logging.warning(str(i)+" add tile "+str(newtile.id)+" "+str(newtile.title))
3327
3328 newtileset.tiles.append(newtile)
3329 db.session.commit()
3330
3331
3332 session["is_client_active"]=True
3333
3334 if (connectionbool):
3335 message = '{"oldtilesetid":'+str(newtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
3336 return redirect(url_for(".addconnection",message=message))
3337 else:
3338 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
3339 return redirect(url_for(".editsession",message=message))
3340
3341 return render_template("edittileset.html", **(myrender()), title="New TileSet TiledViz", form=myform, message=message)
3342
3343
3344# Copy an old TileSet
3345# Only copy old tiles in DB
3346@app.route('/copytileset', methods=["GET", "POST"])
3347def copytileset():
3348 if ("username" in session):
3349 if (session["username"] == "Anonymous"):
3350 return redirect("/login")
3351 else:
3352 flash("Copy TileSet : User must login !")
3353 return redirect("/login")
3354 if ( not "sessionname" in session ):
3355 return redirect("/allsessions")
3356
3357 # Guard: edit permission required on owning project
3358 try:
3359 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name(session["sessionname"])).first()
3360 user_id=get_user_id("CopyTileSetPerm",session["username"])
3361 if not ThisSession or not can_manage_project(ThisSession.id_projects, user_id):
3362 flash("You don't have permission to copy tilesets in this project.")
3363 return redirect("/allsessions")
3364 except Exception:
3365 flash("Unable to validate permissions for tileset copy.")
3366 return redirect("/allsessions")
3367
3368 message=json.loads(request.args["message"])
3369 logging.warning("copytileset : "+str(message))
3370 oldtilesetid=message["oldtilesetid"]
3371 oldtileset=db.session.query(models.TileSets).filter_by(id=oldtilesetid).scalar()
3372
3373 myform = BuildTilesSetForm(oldtileset,onlycopy=True)()
3374
3375 flash("Tileset {} copy for user {} in session {}".format(oldtileset.name,session["username"],session["sessionname"]))
3376 if myform.validate_on_submit():
3377 logging.info("in copy tileset")
3378
3379 if (myform.name.data == oldtileset.name):
3380 message = '{"oldtilsetid":"'+str(oldtilesetid)+'"}'
3381 flash("You must change tilsetname to copy tileset {}".format(oldtileset.name))
3382 #return redirect(url_for(".copytileset",message=message))
3383 return render_template("main_login.html", **(myrender()), title="Copy tileset TiledViz", form=myform, message=message)
3384
3385 nbr_of_tiles = len(oldtileset.tiles)
3386
3387 sessioncopy=db.session.query(models.Sessions).filter_by(models.Sessions.name.like(session["sessionname"])).scalar()
3388 creation_date=datetime.datetime.now()
3389 tilesetname=myform.name.data
3390 newtileset, exist=create_newtileset(myform.name.data, sessioncopy, oldtileset.type_of_tiles, oldtileset.Dataset_path, creation_date)
3391 if (not exist):
3392 try:
3393 db.session.add(newtileset)
3394 db.session.commit()
3395 except Exception:
3396 traceback.print_exc(file=sys.stderr)
3397
3398 flash("TileSet creation with name {} already exist :".format(tilesetname))
3399 return render_template("main_login.html", **(myrender()), title="New TileSet TiledViz", form=myform, message=message)
3400
3401
3402 newtileset.tiles=[]
3403 for i in range(nbr_of_tiles):
3404 oldtile=oldtileset.tiles[i]
3405 newtileset.tiles.append(oldtile)
3406 db.session.commit()
3407
3408 session["is_client_active"]=True
3409
3410 # TODO : get back old connection if exists
3411 urlbool=False
3412 connectionbool=False
3413 if (oldtileset.type_of_tiles == "URL"):
3414 urlbool=True
3415 elif(oldtileset.type_of_tiles == "CONNECTION"):
3416 connectionbool=True
3417
3418 if (connectionbool):
3419 # Copy a mirror connection
3420 copy_tileset_connection(oldtileset,newtileset,session["sessionname"])
3421 logging.error("copy id connection : "+str(newtileset.id_connections))
3422
3423 flag_modified(newtileset,"config_files")
3424 db.session.commit()
3425
3426 message = '{"oldtilesetid":'+str(newtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
3427 return redirect(url_for(".edittileset",message=message))
3428 else:
3429 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
3430 return redirect(url_for(".editsession",message=message))
3431
3432 return render_template("edittileset.html", **(myrender()), title="Copy TileSet TiledViz", form=myform, message=message)
3433
3434def save_tiles(oldtileset,nbr_of_tiles,connectionbool,urlbool,datapath,jsonTileSet,creation_date):
3435 oldtileset.tiles=[]
3436 tilesetname=oldtileset.name
3437
3438 for i in range(nbr_of_tiles):
3439 Mynode=jsonTileSet["nodes"][i]
3440 #print (str(i)+" "+str(Mynode))
3441
3442 try:
3443 title,name,comment,tags,variable,pos_px_x,pos_px_y,IdLocation,url,ConnectionPort = \
3444 convertTile(Mynode,tilesetname,connectionbool,urlbool,datapath)
3445 except Exception as e:
3446 etype, value, tb = sys.exc_info()
3447 flash("TileSet with name {} edition problem :".format(tilesetname)+"\n"+
3448 ''.join(traceback.format_tb(tb)))
3449 message=json.loads(request.args["message"].replace("'", '"'))
3450 logging.error(''.join(traceback.format_tb(tb)))
3451 return redirect(url_for(".edittileset",message=message))
3452
3453 # # Insert and create only NEW tiles into TileSet or
3454 # # TODO: edit OLD tiles from TileSet.tiles list ? (add a suppress old tiles button in form).
3455 # # search if Tile already exists :
3456 # try:
3457 # # unicity for (title, comment, tags) (url ?)
3458 # oldtile=db.session.query(models.Tiles).filter_by(title=title,comment=comment).order_by(models.Tiles.id.desc()).first()
3459
3460 # # if (type(oldtile) == type(None)):
3461 # # logging.warning(str(i)+" tile type "+str(type(oldtile)))
3462 # # # search with url ? (if comment has changed)
3463 # # try:
3464 # # oldtile=db.session.query(models.Tiles).filter_by(title=title,source={"name":name,"url":url,"connection":ConnectionPort,"variable":variable}).order_by(models.Tiles.id.desc()).first()
3465 # # => sqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedFunction) operator does not exist: json = unknown
3466 # # LINE 3: WHERE tiles.title = '001 ' AND tiles.source = '{"name": "001...
3467 # # ^
3468 # # HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
3469 # # oldtile=db.session.query(models.Tiles).filter_by(title=title,source=jsonify(name=name,url=url,connection=ConnectionPort,variable=variable)).order_by(models.Tiles.id.desc()).first()
3470 # # TypeError: Object of type Response is not JSON serializable
3471 # # except :
3472 # # raise AttributeError
3473
3474 # logging.debug(str(i)+" tile type "+str(type(oldtile)))
3475 # oldtileid=oldtile.id
3476 # logging.warning(str(i)+" update old tile "+str(oldtileid))
3477 # oldtile.tags=tags
3478 # oldtile.source= {"name" : name,
3479 # "connection" : ConnectionPort,
3480 # "url" : url,
3481 # "variable": variable}
3482 # flag_modified(oldtile,"source")
3483 # oldtile.pos_px_x= pos_px_x
3484 # oldtile.pos_px_y= pos_px_y
3485 # oldtile.IdLocation=IdLocation
3486 # oldtileset.tiles.append(oldtile)
3487 # except AttributeError:
3488 # if not : insert at end of oldtileset.tiles ?
3489 if (True):
3490 newtile = models.Tiles(title=title,
3491 comment=comment,
3492 tags=tags,
3493 source= {"name" : name,
3494 "connection" : ConnectionPort,
3495 "url" : url,
3496 "variable": variable
3497 },
3498 pos_px_x= pos_px_x,
3499 pos_px_y= pos_px_y,
3500 IdLocation=IdLocation,
3501 creation_date= creation_date)
3502 lasttile=db.session.query(models.Tiles.id).order_by(models.Tiles.id.desc()).first()
3503 if ( lasttile ):
3504 newtile.id=lasttile.id+1
3505 else:
3506 newtile.id=1
3507 db.session.add(newtile)
3508 oldtileset.tiles.append(newtile)
3509 logging.warning(str(i)+" add tile "+str(newtile.id))
3510 # except Exception:
3511 # logging.warning(str(i)+" Error tile ")
3512 # traceback.print_exc(file=sys.stderr)
3513
3514 db.session.commit()
3515
3516# Edit old new TileSet
3517@app.route('/edittileset', methods=["GET", "POST"])
3518def edittileset():
3519 if ("username" in session):
3520 if (session["username"] == "Anonymous"):
3521 return redirect("/login")
3522 else:
3523 flash("Edit TileSet : User must login !")
3524 return redirect("/login")
3525 if ( not "sessionname" in session ):
3526 return redirect("/allsessions")
3527
3528 try:
3529 message=json.loads(request.args["message"].replace("'", '"'))
3530 except json.decoder.JSONDecodeError as e:
3531 logging.error("message error ! "+str(e))
3532 logging.error("message : "+str(request.args["message"]))
3533 traceback.print_exc(file=sys.stderr)
3534
3535 logging.warning("edittileset : "+str(message))
3536
3537 oldtilesetid=message["oldtilesetid"]
3538 oldtileset=db.session.query(models.TileSets).filter_by(id=oldtilesetid).one()
3539
3540 # TODO : test if user is in a session with this tileset
3541 # Guard: edit permission required on owning project
3542 try:
3543 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
3544 user_id=get_user_id("EditTileSetPerm",session["username"])
3545 if not ThisSession or not can_manage_project(ThisSession.id_projects, user_id):
3546 flash("You don't have permission to edit tilesets in this project.")
3547 return redirect("/allsessions")
3548 except Exception:
3549 flash("Unable to validate permissions for tileset edition.")
3550 return redirect("/allsessions")
3551
3552 # Detect how the data of tileset has been inserted :
3553 buildargs={}
3554 buildargs["oldtileset"]=oldtileset
3555
3556 # - PCA -
3557 # |_ buildargs used to pass arguments to BuildTilesSetForm
3558 # |_ there is the json_tiles_text in corresponding to the json of the tileset
3559
3560 if ( session["sessionname"] in jsontransfert):
3561 if ( "TheJson" in jsontransfert[session["sessionname"]]):
3562 # if TheJson is already define in message, it has been edited by jsoneditor (call beside)
3563 logging.debug("TheJson is already define in message")
3564 TheJson=jsontransfert[session["sessionname"]]["TheJson"]
3565 jsontransfert[session["sessionname"]].pop("TheJson")
3566 try:
3567 json_tiles_text=json.JSONEncoder().encode(TheJson)
3568 #print("edittileset json_tiles_text) ",json_tiles_text)
3569 buildargs["json_tiles_text"]=json_tiles_text
3570 except:
3571 traceback.print_exc(file=sys.stderr)
3572 flash("Error from json editor. Please try again.")
3573 return redirect(url_for(".edittileset",message=message))
3574
3575 connectionbool=False
3576 if(oldtileset.type_of_tiles == "CONNECTION"):
3577 # Try to get the old connection
3578 oldConnection_id=-1
3579 try:
3580 oldconnection=db.session.query(models.Connections).filter_by(id=oldtileset.id_connections).one()
3581 oldConnection_id=oldtileset.id_connections
3582 buildargs["editconnection"]=True
3583
3584 except sqlalchemy.orm.exc.NoResultFound:
3585 flash("Tileset {} edit for user {} : no connection found ! ".format(oldtileset.name,session["username"]))
3586 oldConnection_id = 0
3587 buildargs["editconnection"]=True
3588 except AttributeError as err:
3589 #message = '{"oldtilesetid":'+str(oldtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
3590 #return redirect(url_for(".addconnection",message=message))
3591 traceback.print_exc(file=sys.stderr)
3592 logging.error("Error get old connection for tileset %s : %s" % ( oldtileset.name, err ))
3593 flash("Tileset {} edit for user {} : AttributeError ! ".format(oldtileset.name,session["username"]))
3594
3595 except Exception as err:
3596 traceback.print_exc(file=sys.stderr)
3597 logging.error("Error get old connection for tileset %s : %s" % ( oldtileset.name, err ))
3598 flash("Error get old connection for tileset %s : %s" % ( oldtileset.name, err ))
3599
3600 if ( oldConnection_id < 0):
3601 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
3602 return redirect(url_for(".editsession",message=message))
3603 connectionbool=True
3604
3605 myform = BuildTilesSetForm(**buildargs)()
3606
3607 if ("username" in session):
3608 flash("Tileset {} edit for user {} in session {}".format(oldtileset.name,session["username"],session["sessionname"]))
3609 else:
3610 flash("You are not connected. You must login before using a connection.")
3611 return redirect("/login")
3612
3613 if myform.validate_on_submit():
3614 logging.info("in tileset editor")
3615
3616 if(connectionbool and buildargs["editconnection"]):
3617 if (myform.editconnection.data):
3618 message = '{"oldtilesetid":'+str(oldtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
3619 return redirect(url_for(".editconnection",message=message))
3620 if (myform.shellconnection.data):
3621 message = '{"direct":1,"oldtilesetid":'+str(oldtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
3622 return redirect(url_for(".editconnection",message=message))
3623
3624 if(myform.goback.data):
3625 logging.debug("go back to edit old session : "+session["sessionname"])
3626 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
3627 return redirect(url_for(".editsession",message=message))
3628
3629 # - PCA -
3630 # |_ process the anatreada script
3631 logging.info("Calling pca_nodes methode of anatread")
3632 logging.info("The user as selected the option : " + myform.has_pca.data)
3633
3634 # Detect how the data of tiles has been inserted :
3635 if (myform.json_tiles_file.data) :
3636 json_tiles_file = myform.json_tiles_file.data
3637 logging.warning("Read json_tiles_file :"+myform.json_tiles_file.data.filename)
3638
3639 # - PCA -
3640 # if has_pca option is True -> make the PCA over the file
3641 if myform.has_pca.data == "YES" :
3642 json_tiles_text=json_tiles_file.read()
3643 json_pca_tiles_text = anatreada.pca_on_one_node(json_tiles_text)
3644 json_tiles = json_pca_tiles_text
3645 else:
3646 json_tiles = json_tiles_file.read()
3647 else:
3648 # if json structure is inserted with text area
3649
3650 # - PCA -
3651 # if has_pca option is True -> make the PCA over the text area
3652 if myform.has_pca.data == "YES" :
3653 json_pca_tiles_text = anatreada.pca_on_one_node(myform.json_tiles_text.data)
3654 json_tiles = json_pca_tiles_text
3655 logging.info("edittileset -> myform.has_pca.data = YES -> json_tiles")
3656 else:
3657 json_tiles = myform.json_tiles_text.data
3658
3659 try:
3660 if myform.editjson.data:
3661 jsontransfert[session["sessionname"]]={"callfunction": '{"function":"edittileset",'+'"args":{"oldtilesetid":"'+str(oldtilesetid)+'"}}',
3662 "TheJson":json_tiles}
3663 # json_gziped=base64.b64encode(gzip.compress(json_tiles.encode('utf-8')))
3664 #return redirect(url_for(".jsoneditor",callfunction=callfunction,TheJson=json_gziped))
3665 return redirect(url_for(".jsoneditor"))
3666 except Exception as err:
3667 traceback.print_exc(file=sys.stderr)
3668 logging.error("Error editjson %s : %s" % ( str(myform.editjson), err ))
3669
3670 if(oldtileset.type_of_tiles == "CONNECTION" and myform.manage_connection.data != "reNew" and not myform.createconnection.data):
3671 user_id=get_user_id("edittileset",session["username"])
3672 if ( not "connection"+str(oldConnection_id) in session):
3673 flash("You don't have connection information in your personal cookie for this connection.")
3674 logging.error("You (user "+str(user_id)+") don't have connection information in your personal cookie for this connection : "+str(oldConnection_id))
3675 message=request.args["message"]
3676 return redirect(url_for(".edittileset",message=message))
3677
3678 # Build connection path
3679 user_path=os.path.join("/TiledViz/TVFiles",str(user_id))
3680 connectionpath=os.path.join(user_path,str(oldConnection_id))
3681
3682 # Diff config files to write them if needed in connection path
3683 oldtileset_config_files=oldtileset.config_files
3684 if (myform.configfiles.data):
3685 for FileS in myform.configfiles.data:
3686 # data from form
3687 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
3688 tf.write(FileS.read())
3689 newfilename=tf.name
3690 tf.close()
3691 if (os.stat(tf.name).st_size > 0):
3692 if (FileS.filename in oldtileset_config_files):
3693 # Test file modified with same name
3694 boolDiff=filecmp.cmp(f1=oldtileset_config_files[FileS.filename],f2=newfilename)
3695 logging.warning("Diff with modified config file : "+str(boolDiff))
3696 if (not boolDiff):
3697 # rm old config files
3698 strrm="rm -f "+oldtileset_config_files[FileS.filename]
3699 logging.warning("Update old config file "+FileS.filename+" in edittileset.")
3700 os.system(strrm)
3701 oldtileset.config_files[FileS.filename]=newfilename
3702 flag_modified(oldtileset,"config_files")
3703 db.session.commit()
3704 else:
3705 # rm unused config files
3706 strrm="rm -f "+newfilename
3707 os.system(strrm)
3708 else:
3709 # new config file
3710 oldtileset.config_files[FileS.filename]=newfilename
3711 logging.warning("Add new config file "+FileS.filename+" in edittileset.")
3712 flag_modified(oldtileset,"config_files")
3713 db.session.commit()
3714 else:
3715 # rm unused config files
3716 strrm="rm -f "+newfilename
3717 os.system(strrm)
3718
3719 # Python file to launch case
3720 launch_file=myform.script_launch_file.data
3721
3722 oldtileset_launch_file=oldtileset.launch_file
3723 FileS=myform.script_launch_file.data
3724 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
3725 tf.write(FileS.read())
3726 newfilename=tf.name
3727 tf.close()
3728 if (os.stat(tf.name).st_size > 0):
3729 if (FileS.filename in oldtileset_config_files):
3730 # Test launch_file modified with same name
3731 boolDiff=filecmp.cmp(f1=oldtileset_config_files[FileS.filename],f2=newfilename)
3732 logging.warning("Diff with modified launch case file : "+str(boolDiff))
3733 if (not boolDiff):
3734 # rm old config files
3735 strrm="rm -f "+oldtileset_config_files[FileS.filename]
3736 logging.warning("Update old launch file "+FileS.filename+" in edittileset.")
3737 os.system(strrm)
3738 oldtileset.config_files[FileS.filename]=newfilename
3739 flag_modified(oldtileset,"config_files")
3740 db.session.commit()
3741 else :
3742 # rm unused config files
3743 strrm="rm -f "+newfilename
3744 os.system(strrm)
3745 else :
3746 # New launch_file filename
3747 # rm old config files
3748 strrm="rm -f "+oldtileset_config_files[oldtileset_launch_file]
3749 del(oldtileset_config_files[oldtileset_launch_file])
3750 logging.warning("Rename launch file from "+oldtileset_launch_file
3751 +" to "+FileS.filename+" in edittileset.")
3752 os.system(strrm)
3753 oldtileset.launch_file=FileS.filename
3754 oldtileset.config_files[FileS.filename]=newfilename
3755 flag_modified(oldtileset,"config_files")
3756 db.session.commit()
3757 else :
3758 # rm unused config files
3759 strrm="rm -f "+newfilename
3760 os.system(strrm)
3761
3762 elif(oldtileset.type_of_tiles == "CONNECTION" and (myform.manage_connection.data == "reNew" or myform.createconnection.data)):
3763 launch_file=myform.script_launch_file.data
3764 if (not launch_file):
3765 flash("TileSet with connection must have at least a script to launch your tiles.\n You must add launch_file script.")
3766 return render_template("main_login.html", **(myrender()), title="Edit TileSet TiledViz", form=myform, message=message)
3767 # Register config files in jsontransfert to write them in connection path
3768 TStmpName="tileset_"+str(oldtileset.id)
3769 if ( not TStmpName in jsontransfert):
3770 jsontransfert[TStmpName]={}
3771 if (myform.configfiles.data):
3772 for FileS in myform.configfiles.data:
3773 jsontransfert[TStmpName][FileS.filename]=FileS.read()
3774
3775 # Python file to launch case
3776 jsontransfert["tileset_"+str(oldtileset.id)][launch_file.filename]=launch_file.read()
3777 oldtileset.launch_file=launch_file.filename
3778 db.session.commit()
3779
3780
3781 # Translate json text in structure
3782 if (len(json_tiles) > 0):
3783 jsonTileSet = json.loads(json_tiles)
3784 else:
3785 jsonTileSet = {"nodes":[]}
3786 # else:
3787 # json_file_name=secure_filename(myform.json_tiles_file.data)
3788 # print("instance_path :",app.instance_path)
3789 # myform.save(os.path.join(app.instance_path, filename))
3790 # req, resp = app.test_client.post(
3791 # '/upload', data={'upload': open(__file__, 'rb')})
3792 # assert resp.status == 200
3793 # assert resp.text == os.path.basename(__file__)
3794 # #json_file = json_file_name.data.read()
3795 # print("Read the file ",json_file_name," contains ",json_file)
3796 # # get call message and put new json from file
3797 # OutJson=json.dumps(json.loads(json_file)).replace("'", '"')
3798 # message["TheJson"]=str(base64.b64encode(gzip.compress(OutJson.encode('utf-8'))))
3799 # message=json.dumps(message) #.replace("'", '"')
3800 # print("Relaunch whith file ",message)
3801 # return redirect(url_for(".edittileset",message=message))
3802
3803
3804 # - PCA -
3805 # use this variable as the knee number
3806 # intialize this varibale with knee number previously calculated if there is an oldtileset
3807 try:
3808 nbr_of_tiles = len(jsonTileSet["nodes"])
3809 logging.info("Number of tiles "+str(nbr_of_tiles))
3810 except Exception as e:
3811 return redirect(url_for(".edittileset",message=message))
3812
3813 # TODO:
3814 # openports_between_tiles = FieldList(IntegerField("port :",validators=[Optional()]),description="Open port in visualisation network",min_entries=2,max_entries=5)
3815
3816 # TODO
3817 # TilesSetForm.script_launch_file
3818
3819
3820 # Insert and create NEW tiles into TileSet or delete OLD tiles from TileSet.tiles list ?
3821 old_nbr_of_tiles=len(oldtileset.tiles)
3822 if (nbr_of_tiles > old_nbr_of_tiles):
3823 insertnewtiles=True
3824 elif (nbr_of_tiles < old_nbr_of_tiles):
3825 deletesometiles=True
3826
3827
3828 urlbool=False
3829 if (myform.type_of_tiles.data == "URL"):
3830 urlbool=True
3831
3832 creation_date=datetime.datetime.now()
3833 if (myform.dataset_path.data):
3834 datapath=str(myform.dataset_path.data)
3835 else:
3836 if (urlbool):
3837 datapath="https"
3838 else:
3839 datapath=""
3840 oldtileset.datapath=datapath
3841 oldtileset.creation_date=creation_date
3842 db.session.commit()
3843
3844
3845 session["is_client_active"]=True
3846
3847 # Get back old connection if exists
3848 if (connectionbool and myform.manage_connection.data):
3849 message = '{"oldtilesetid":'+str(oldtileset.id)+',"oldsessionname":"'+session["sessionname"]+'"}'
3850
3851 if (myform.manage_connection.data == "reNew" or myform.createconnection.data):
3852 return redirect(url_for(".addconnection",message=message))
3853 # elif (myform.manage_connection.data == "Edit"):
3854 # return redirect(url_for(".editconnection",message=message))
3855 elif (myform.manage_connection.data == "Quit"):
3856 return redirect(url_for(".removeconnection",message=message))
3857 else:
3858 save_tiles(oldtileset,nbr_of_tiles,connectionbool,urlbool,datapath,jsonTileSet,creation_date)
3859 return redirect(url_for(".editsession",message=message))
3860 #TODO:
3861 # ("New","Create a new one."),
3862 # ("Save","Save the connection for reuse."),
3863 # ("Reload","Reload saved connection."),
3864 else:
3865 save_tiles(oldtileset,nbr_of_tiles,connectionbool,urlbool,datapath,jsonTileSet,creation_date)
3866 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
3867 return redirect(url_for(".editsession",message=message))
3868
3869 return render_template("edittileset.html", **(myrender()), title="Edit TileSet TiledViz", form=myform, message=message)
3870
3871# Special random function for link keys
3872def linkrandom(nbchar):
3873 ALPHABET = "B6P8VbhZoGp9JYd0uLCsAT4DXF1xqIUSyQMniNgje5_~3crvlHR-7W2f=kEtmazwKO"
3874 mystring=''.join(random.choice(ALPHABET) for i in range(nbchar)).encode('utf-8')
3875 return mystring
3876
3877# Build iframe with noVNC (in template/noVNC ?) inside a comeback html script to be abble to go back with new message
3878# Then kill connection link
3879@app.route('/vncconnection', methods=['GET', 'POST'])
3880def vncconnection():
3881 if ("username" in session):
3882 if (session["username"] == "Anonymous"):
3883 return redirect("/login")
3884 else:
3885 flash("VNC connection : User must login !")
3886 return redirect("/login")
3887 logging.warning("Enter in connection.")
3888
3889 myflush()
3890
3891 try:
3892 message=json.loads(request.args["message"])
3893 except json.decoder.JSONDecodeError as e:
3894 logging.error("message error ! "+str(e))
3895 logging.error("message : "+str(request.args["message"]))
3896 traceback.print_exc(file=sys.stderr)
3897 message=json.loads(request.args["message"].replace("'", '"'))
3898
3899 idconnection=message["connectionid"]
3900 idtileset=message["oldtilesetid"]
3901 try:
3902 oldconnection=db.session.query(models.Connections).filter_by(id=idconnection).first()
3903 except:
3904 flash("This connection doesn't exist.")
3905 logging.error("This connection doesn't exist.")
3906 message=request.args["message"]
3907 return redirect(url_for(".edittileset",message=message))
3908
3909 if ("username" in session):
3910 user_id=get_user_id("vncconnection",session["username"])
3911 else:
3912 flash("You are not connected. You must login before using a connection.")
3913 return redirect("/login")
3914 if ( not "connection"+str(idconnection) in session):
3915 flash("You don't have connection information in your personal cookie for this connection.")
3916 logging.error("You (user "+str(user_id)+") don't have connection information in your personal cookie for this connection : "+str(idconnection))
3917 message=request.args["message"]
3918 return redirect(url_for(".edittileset",message=message))
3919
3920 vnctransfert=json.loads(session["connection"+str(idconnection)])
3921 logging.debug("With infos :"+str(vnctransfert))
3922
3923 flaskaddr=os.getenv("flaskhost")
3924 logging.debug("Detected flask address :"+str(flaskaddr))
3925
3926 callfunction=vnctransfert["callfunction"]
3927 if (oldconnection):
3928 if (user_id != oldconnection.id_users) :
3929 flash("You can not access to this connection. You are not its owner.")
3930 owner=db.session.query(models.Users).filter_by(id=oldconnection.id_users).one().name
3931 you=db.session.query(models.Users).filter_by(id=user_id).one().name
3932 logging.error("You (user "+you+") can not access to this connection owned by user "+owner)
3933 message=request.args["message"]
3934 return redirect(url_for(".edittileset",message=message))
3935
3936 # Wait from TVSecure for connection PORT in DB
3937 db.session.refresh(oldconnection)
3938 connection_vnc=oldconnection.connection_vnc
3939 logging.warning("PORT VNC :"+str(connection_vnc))
3940 if (connection_vnc == 0):
3941 flash_msg="Error reading PORT VNC :"+str(connection_vnc)
3942 logging.error(flash_msg)
3943 flash(flash_msg)
3944 message=request.args["message"]
3945 return redirect(url_for(".edittileset",message=message))
3946 connection_vnc=connection_vnc+32768
3947
3948 if ( request.method == 'POST'):
3949 logging.warning("in POST")
3950 myflush()
3951 message=json.JSONEncoder().encode(vnctransfert["args"])
3952
3953 logging.warning("killconnection: "
3954 +str(session["username"])+" ; "
3955 +str(idtileset)+" ; "
3956 +str(idconnection))
3957 myflush()
3958
3959 out_nodes_json = os.path.join("/TiledViz/TVFiles", str(user_id), str(idconnection),"nodes.json")
3960 logging.warning("out_nodes_json after vncconnection.html :"+out_nodes_json)
3961
3962 # Wait NbTimeAlive to get files from connection
3963 NbTimeAlive=20
3964
3965 if ( not session["sessionname"] in jsontransfert):
3966 jsontransfert[session["sessionname"]]={}
3967
3968 count=0
3969 while True:
3970 time.sleep(timeAlive)
3971 if (count > NbTimeAlive):
3972 if ("TheJson" in jsontransfert[session["sessionname"]]):
3973 del(jsontransfert[session["sessionname"]]["TheJson"])
3974 return redirect(url_for("."+callfunction,message=message))
3975 if ( os.path.exists( out_nodes_json ) ):
3976 try:
3977 json_tiles_file=open(out_nodes_json)
3978 jsontransfert[session["sessionname"]]["TheJson"]=json.loads(json_tiles_file.read())
3979 json_tiles_file.close()
3980 logging.warning("nodes.json read and OK "+out_nodes_json)
3981 return redirect(url_for("."+callfunction,message=message))
3982 except Exception as err:
3983 traceback.print_exc(file=sys.stderr)
3984 strerror="Error from json "+out_nodes_json+" file from connection : "+str(err)
3985 logging.error(strerror)
3986 else:
3987 logging.warning("Wait for "+out_nodes_json)
3988 count=count+1
3989
3990 return render_template("vncconnection.html", **(myrender()),
3991 port=connection_vnc,
3992 id=idconnection,
3993 tsid=idtileset,
3994 vncpassword=vnctransfert["vncpassword"],
3995 session=session["sessionname"],
3996 flaskaddr=flaskaddr)
3997
3998@app.route('/addconnection', methods=["GET", "POST"])
3999def addconnection():
4000 global TimeConnection
4001
4002 if ("username" in session):
4003 if (session["username"] == "Anonymous"):
4004 return redirect("/login")
4005 else:
4006 flash("Add connection : User must login !")
4007 return redirect("/login")
4008
4009 User = db.session.query(models.Users).filter_by(name=session["username"]).one()
4010
4011 logging.warning(f"authchoices before {authchoice}")
4012 myform = BuildConnectionsForm(is_admin=User.is_admin,authchoice=authchoice)()
4013
4014 print('message=',str(request.args["message"]))
4015 message=json.loads(request.args["message"])
4016 message["TimeConnection"]=TimeConnection
4017
4018 logging.debug("ConnectionForm built."+str(message))
4019
4020 # Guard: edit permission required on owning project
4021 try:
4022 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
4023 user_id=get_user_id("AddConnectionPerm",session["username"])
4024 if not ThisSession or not can_manage_project(ThisSession.id_projects, user_id):
4025 flash("You don't have permission to manage connections in this project.")
4026 return redirect("/allsessions")
4027 except Exception:
4028 flash("Unable to validate permissions for connection management.")
4029 return redirect("/allsessions")
4030
4031 if myform.validate_on_submit():
4032 logging.info("in addconnection")
4033
4034
4035 # TODO :
4036 # test validity of data with s.encode('ascii') and except UnicodeDecodeError:
4037 #myform.host_address.data myform.auth_type.data myform.container.data ?
4038
4039
4040 logging.info(str(session["username"])+" "+str(myform.host_address.data)+" "+str(myform.auth_type.data)+" "+str(myform.container.data))
4041
4042 creation_date=datetime.datetime.now()
4043 user_id=get_user_id("addconnection",session["username"])
4044
4045 # Test if a connection is already attached few seconds ago for the tileset :
4046 newtileset=db.session.query(models.TileSets).filter_by(id=message["oldtilesetid"]).one()
4047
4048 if (type(newtileset.id_connections) != type(None)):
4049 logging.warning("New connection created :"+str(newtileset.id_connections))
4050 try:
4051 oldConnection=db.session.query(models.Connections).filter_by(id=newtileset.id_connections).one()
4052 olddate=oldConnection.creation_date
4053 if ((creation_date-olddate).seconds < 3):
4054 return
4055 except:
4056 return
4057
4058 if (myform.scheduler_file.data):
4059 scheduler_filename=myform.scheduler_file.data.filename
4060 else:
4061 scheduler_filename=""
4062
4063 newConnection = models.Connections(host_address=myform.host_address.data,
4064 auth_type=myform.auth_type.data,
4065 container=myform.container.data,
4066 scheduler=myform.scheduler.data,
4067 scheduler_file=scheduler_filename,
4068 id_users=user_id,
4069 creation_date= creation_date)
4070
4071 lastconnection=db.session.query(models.Connections.id).order_by(models.Connections.id.desc()).first()
4072 if ( lastconnection ):
4073 newConnection.id=lastconnection.id+1
4074 else:
4075 newConnection.id=1
4076 db.session.add(newConnection)
4077 db.session.commit()
4078
4079 # We must add connection id in the tile_set
4080 newtileset.id_connections=newConnection.id
4081 db.session.commit()
4082
4083 # TODO : Add subdir specific ?
4084 TSConfigjson={}
4085 ConnConfigjson={}
4086 # Build connection path
4087 user_path=os.path.join("/TiledViz/TVFiles",str(user_id))
4088 connectionpath=os.path.join(user_path,str(newConnection.id))
4089 # Create connection dir
4090 try:
4091 os.mkdir(user_path)
4092 logging.warning("Creation of connection path for config files : "+user_path)
4093 except FileExistsError:
4094 pass
4095 try:
4096 os.mkdir(connectionpath)
4097 logging.warning("Creation of connection path for config files : "+connectionpath)
4098 except FileExistsError:
4099 pass
4100
4101 # Save config files from TileSet (placed in jsontransfer) and connection in this connectionpath
4102 TStmpName="tileset_"+str(newtileset.id)
4103 if ( TStmpName in jsontransfert):
4104 for FileS in jsontransfert[TStmpName]:
4105 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
4106 tf.write(jsontransfert[TStmpName][FileS])
4107 # TODO : Add dir ?
4108 TSConfigjson[FileS]=tf.name
4109 tf.close()
4110 # for FileS in jsontransfert[TStmpName]:
4111 # jsontransfert[TStmpName].pop(FileS)
4112 jsontransfert.pop(TStmpName)
4113
4114 # Write config files for connection
4115 if ( myform.configfiles.data and str(myform.configfiles.data) != str([""]) ):
4116 logging.info("Config files : %s " % (str(myform.configfiles.data)))
4117 for FileS in myform.configfiles.data:
4118 logging.warning("Config files File : %s " % (str(FileS)))
4119 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
4120 tf.write(FileS.read())
4121 # TODO : Add dir (in JOBPath on HPC machine) info for files ?
4122 ConnConfigjson[FileS.filename]=tf.name
4123 tf.close()
4124
4125 # if (myform.configfiles.data):
4126 # for FileS in myform.configfiles.data:
4127 # logging.error("Config type file : %s " % (str(type(FileS))))
4128 # outHandler.flush()
4129 # if ( type(FileS) != type("AA") ):
4130 # logging.error("Config file : %s " % (FileS))
4131 # tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
4132 # tf.write(myform.configfiles.data[FileS].read())
4133 # # TODO : Add dir (in JOBPath on HPC machine) info for files ?
4134 # ConnConfigjson[FileS]=tf.name
4135 # tf.close()
4136 # else:
4137 # logging.warning("Config file : %s " % (FileS))
4138 # outHandler.flush()
4139
4140 # Save scheduler_file in connectionpath and tmp filename in ConnConfigjson
4141 if (myform.scheduler_file.data):
4142 logging.warning("Scheduler file : %s " % (str(myform.scheduler_file.data)))
4143 scheduler_file=myform.scheduler_file.data
4144 if ( scheduler_file != "" ):
4145 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
4146 tf.write(scheduler_file.read())
4147 ConnConfigjson[scheduler_file.filename]=tf.name
4148 tf.close()
4149
4150 sTSConfigjson=str(TSConfigjson).replace("'", '"')
4151 logging.warning("Configuration files for TileSet %s : %s " % (newtileset.name,sTSConfigjson) )
4152 newtileset.config_files=json.loads(sTSConfigjson)
4153
4154 sConnConfigjson=str(ConnConfigjson).replace("'", '"')
4155 logging.warning("Configuration files for connection %d : %s " % (newConnection.id,sConnConfigjson) )
4156 newConnection.config_files=json.loads(sConnConfigjson)
4157 db.session.commit()
4158
4159 deb=(myform.debug.data & 1 | 0)
4160
4161 passpath="/home/connect"+str(newConnection.id)+"/vncpassword"
4162 logging.warning("Go to vnc with path "+passpath)
4163
4164 config = configparser.ConfigParser()
4165 config.optionxform = str
4166
4167 if ( "config.tar" in newtileset.config_files ):
4168 tar_config_file=tarfile.TarFile(name=newtileset.config_files["config.tar"],mode='r')
4169 tar_config_file.list()
4170 try:
4171 with tar_config_file.extractfile("case_config.ini") as case_config_file:
4172 config.read_string(case_config_file.read().decode('utf-8'))
4173 nbtiles=int(config['CASE']['NUM_DOCKERS'])
4174 except Exception as err:
4175 traceback.print_exc(file=sys.stderr)
4176 strerror="Error getting number of tiles from case_config in config.tar before connection : "+str(err)
4177 logging.error(strerror)
4178 nbtiles=0
4179 else:
4180 try:
4181 config.read(newtileset.config_files["case_config.ini"])
4182 nbtiles=int(config['CASE']['NUM_DOCKERS'])
4183 except Exception as err:
4184 traceback.print_exc(file=sys.stderr)
4185 strerror="Error getting number of tiles from case_config.ini before connection : "+str(err)
4186 logging.error(strerror)
4187 nbtiles=0
4188
4189 logging.warning("addconnection: "
4190 +str(session["username"])+" ; "
4191 +str(myform.host_address.data)+" ; "
4192 +str(myform.auth_type.data)+" ; "
4193 +str(myform.container.data)+" ; "
4194 +str(myform.scheduler.data)+" ; "
4195 +str(newtileset.id)+" ; "
4196 +str(newConnection.id)+" ; "
4197 +str(nbtiles)+" ; "
4198 +str(deb))
4199 myflush()
4200
4201 # Wait NbTimeAlive for TVSecure to get VNC view to put connection datas.
4202 NbTimeAlive = 40
4203 count=0
4204 countTimeConnection=time.time()
4205 while(True):
4206 if (count > NbTimeAlive):
4207 strerror="Connection has never been reach. Go back to TileSet."
4208 logging.error(strerror)
4209 flash(strerror)
4210 message = '{"oldtilesetid": "'+str(newtileset.id)+'"}'
4211 return redirect(url_for(".edittileset",message=message))
4212 count=count+1
4213 #logging.warning("addconnection count : "+str(count))
4214
4215 # GET VNC password in
4216 # security problem here if server is attacked ?
4217 time.sleep(timeAlive)
4218 #os.system("ls -la "+passpath)
4219 #sys.stdout.flush()
4220 if (os.path.isfile(passpath)):
4221 countTimeConnection=int(time.time()-countTimeConnection)
4222 if (TimeConnection!=countTimeConnection):
4223 TimeConnection=countTimeConnection
4224 with open(passpath,'r') as f:
4225 vncpassword=re.sub(r'\n',r'',f.read())
4226 f.close()
4227 logging.debug("and password : "+vncpassword)
4228
4229 message = '{"oldtilesetid":'+str(newtileset.id)+',"connectionid":'+str(newConnection.id)+',"sessionname":"'+session["sessionname"]+'"}'
4230 session["connection"+str(newConnection.id)]=' {"callfunction":"edittileset",'+'"args":{"oldsessionname":"'+str(session["sessionname"])+'","oldtilesetid":"'+str(newtileset.id)+'"}, "vncpassword":"'+vncpassword+'"}'
4231
4232 logging.warning("addconnection in session : "+str(session["connection"+str(newConnection.id)]))
4233 logging.warning("Go to vncconnection : "+str(url_for(".vncconnection",message=message)))
4234 #TODO logging.debug
4235 myflush()
4236 return redirect(url_for(".vncconnection",message=message))
4237
4238 return render_template("addconnection.html", **(myrender()), title="Add new Connection TiledViz", form=myform, message=message)
4239
4240# Edit old Connection related to a tileset
4241@app.route('/editconnection', methods=["GET", "POST"])
4242def editconnection():
4243 logging.warning('editconnection message='+str(request.args["message"]))
4244
4245 if ("username" in session):
4246 if (session["username"] == "Anonymous"):
4247 return redirect("/login")
4248 else:
4249 flash("Edit connection : User must login !")
4250 return redirect("/login")
4251
4252 # Guard: edit permission required on owning project
4253 try:
4254 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
4255 user_id=get_user_id("EditConnectionPerm",session["username"])
4256 if not ThisSession or not can_manage_project(ThisSession.id_projects, user_id):
4257 flash("You don't have permission to edit connections in this project.")
4258 return redirect("/allsessions")
4259 except Exception:
4260 flash("Unable to validate permissions for connection edition.")
4261 return redirect("/allsessions")
4262
4263 try:
4264 message=json.loads(request.args["message"])
4265 except json.decoder.JSONDecodeError as e:
4266 logging.error("message error ! "+str(e))
4267 logging.error("message : "+str(request.args["message"]))
4268 traceback.print_exc(file=sys.stderr)
4269 message=json.loads(request.args["message"].replace("'", '"'))
4270
4271 oldtilesetid=message["oldtilesetid"]
4272 oldtileset=db.session.query(models.TileSets).filter_by(id=oldtilesetid).one()
4273
4274 oldconnection=oldtileset.connections
4275 try:
4276 idconnection=oldconnection.id
4277 except:
4278 flash("This connection doesn't exist.")
4279 logging.error("This connection doesn't exist.")
4280 message=request.args["message"]
4281 return redirect(url_for(".edittileset",message=message))
4282
4283 user_id=get_user_id("editconnection",session["username"])
4284 # Build connection path
4285 user_path=os.path.join("/TiledViz/TVFiles",str(user_id))
4286 connectionpath=os.path.join(user_path,str(oldconnection.id))
4287
4288
4289 if ( not "connection"+str(idconnection) in session):
4290 flash("You don't have connection information in your personal cookie for this connection.")
4291 logging.error("You (user "+str(user_id)+") don't have connection information in your personal cookie for this connection : "+str(idconnection))
4292 message=request.args["message"]
4293 return redirect(url_for(".edittileset",message=message))
4294
4295 if ("direct" in message):
4296 del message["direct"]
4297 message=launch_connection(oldtileset,oldconnection,
4298 oldconnection.host_address,
4299 oldconnection.auth_type,
4300 oldconnection.container,
4301 oldconnection.scheduler)
4302 return redirect(url_for(".vncconnection",message=message))
4303 flash("Error direct connection to shell.")
4304
4305
4306 User = db.session.query(models.Users).filter_by(name=session["username"]).one()
4307
4308 myform = BuildConnectionsForm(is_admin=User.is_admin,oldconnection=oldconnection)()
4309
4310 logging.debug("ConnectionForm edit."+str(message))
4311 if myform.validate_on_submit():
4312 logging.info("in editconnection")
4313
4314 logging.info(str(myform.host_address.data)+" "+str(myform.auth_type.data)+" "+str(myform.container.data))
4315
4316 TSConfigjson={}
4317
4318 # TODO :
4319 # Test connection type in launch a form dedicated.
4320
4321 # TODO :
4322 # rm old config files not overwitten if exists ?
4323
4324 # detect/diff/update config files
4325 oldconnection_config_files=oldconnection.config_files
4326
4327 if (myform.configfiles.data):
4328 for FileS in myform.configfiles.data:
4329 # data from form
4330 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
4331 tf.write(FileS.read())
4332 newfilename=tf.name
4333 tf.close()
4334 if (os.stat(tf.name).st_size > 0):
4335 if (FileS.filename in oldconnection_config_files):
4336 # Test file modified with same name
4337 boolDiff=filecmp.cmp(f1=oldconnection_config_files[FileS.filename],f2=newfilename)
4338 logging.warning("Diff with modified config file : "+str(boolDiff))
4339 if (not boolDiff):
4340 # rm old config files
4341 strrm="rm -f "+oldconnection_config_files[FileS.filename]
4342 logging.warning("Update old config file "+FileS.filename+" in editconnection.")
4343 os.system(strrm)
4344 oldconnection.config_files[FileS.filename]=newfilename
4345 flag_modified(oldconnection,"config_files")
4346 db.session.commit()
4347 else :
4348 # rm unused config files
4349 strrm="rm -f "+newfilename
4350 os.system(strrm)
4351 else:
4352 # new config file
4353 oldconnection.config_files[FileS.filename]=newfilename
4354 logging.warning("Add new config file "+FileS.filename+" in editconnection.")
4355 flag_modified(oldconnection,"config_files")
4356 db.session.commit()
4357 else :
4358 # rm unused config files
4359 strrm="rm -f "+newfilename
4360 os.system(strrm)
4361
4362 # detect/diff/update scheduler file
4363 if (myform.scheduler_file.data):
4364 oldconnection_scheduler_file=oldconnection.scheduler_file
4365 FileS=myform.scheduler_file.data
4366 tf = tempfile.NamedTemporaryFile(mode="w+b",dir=connectionpath,prefix="",delete=False)
4367 tf.write(FileS.read())
4368 newfilename=tf.name
4369 tf.close()
4370 if (os.stat(tf.name).st_size > 0):
4371 if (FileS.filename in oldconnection_config_files):
4372 # Test scheduler_file modified with same name
4373 boolDiff=filecmp.cmp(f1=oldconnection_config_files[FileS.filename],f2=newfilename)
4374 logging.warning("Diff with modified scheduler file : "+str(boolDiff))
4375 if (not boolDiff):
4376 # rm old config files
4377 strrm="rm -f "+oldconnection_config_files[FileS.filename]
4378 logging.warning("Update old scheduler file "+FileS.filename+" in editconnection.")
4379 os.system(strrm)
4380 oldconnection.config_files[FileS.filename]=newfilename
4381 flag_modified(oldconnection,"config_files")
4382 db.session.commit()
4383 else :
4384 # rm unused config files
4385 strrm="rm -f "+newfilename
4386 os.system(strrm)
4387 else:
4388 if (oldconnection_scheduler_file):
4389 # rm old config files
4390 strrm="rm -f "+oldconnection_config_files[oldconnection_scheduler_file]
4391 logging.warning("Rename scheduler file from "+oldconnection_scheduler_file
4392 +" to "+FileS.filename+" in editconnection.")
4393 os.system(strrm)
4394 else:
4395 logging.warning("Add scheduler file "+FileS.filename+" in editconnection.")
4396 oldconnection.scheduler_file=FileS.filename
4397 oldconnection.config_files[FileS.filename]=newfilename
4398 flag_modified(oldconnection,"config_files")
4399 db.session.commit()
4400 else :
4401 # rm unused config files
4402 strrm="rm -f "+newfilename
4403 os.system(strrm)
4404
4405 message=launch_connection(oldtileset,oldconnection,
4406 myform.host_address.data,
4407 myform.auth_type.data,
4408 myform.container.data,
4409 myform.scheduler.data)
4410 return redirect(url_for(".vncconnection",message=message))
4411
4412 return render_template("main_login.html", **(myrender()), title="Edit Connection TiledViz", form=myform, message=message)
4413
4414
4415# Remove old Connection related to a tileset
4416@app.route('/removeconnection', methods=["GET", "POST"])
4417def removeconnection():
4418 logging.warning('removeconnection message='+str(request.args["message"]))
4419
4420 if ("username" in session):
4421 if (session["username"] == "Anonymous"):
4422 return redirect("/login")
4423 else:
4424 flash("Remove connection : User must login !")
4425 return redirect("/login")
4426
4427 try:
4428 message=json.loads(request.args["message"])
4429 except json.decoder.JSONDecodeError as e:
4430 logging.error("message error ! "+str(e))
4431 logging.error("message : "+str(request.args["message"]))
4432 traceback.print_exc(file=sys.stderr)
4433 message=json.loads(request.args["message"].replace("'", '"'))
4434
4435 oldtilesetid=message["oldtilesetid"]
4436 oldtileset=db.session.query(models.TileSets).filter_by(id=oldtilesetid).one()
4437
4438 oldconnection=oldtileset.connections
4439 try:
4440 idconnection=oldconnection.id
4441 except:
4442 flash("This connection doesn't exist.")
4443 logging.error("This connection doesn't exist.")
4444 message=request.args["message"]
4445 return redirect(url_for(".edittileset",message=message))
4446
4447 user_id=get_user_id("removeconnection",session["username"])
4448 if ( not "connection"+str(idconnection) in session):
4449 flash("You don't have connection information in your personal cookie for this connection.")
4450 logging.error("You (user "+str(user_id)+") don't have connection information in your personal cookie for this connection : "+str(idconnection))
4451 message=request.args["message"]
4452 return redirect(url_for(".edittileset",message=message))
4453
4454 remove_this_connection(oldtileset,idconnection,user_id)
4455
4456 flash("Connection "+str(idconnection)+" for tileset "+oldtileset.name+" has been removed.")
4457 message=request.args["message"]
4458 return redirect(url_for(".edittileset",message=message))
4459
4460
4461# Call json editor on a structure
4462# TODO : no more GET method to test ?
4463@app.route('/jsoneditor', methods=['GET', 'POST'])
4464def jsoneditor():
4465 #print("jsoneditor args : ",request.args)
4466 # json_gziped=request.args["TheJson"]
4467 # #print("type json_gziped :",type(json_gziped))
4468 # TheJson=gzip.decompress(base64.b64decode(json_gziped)).decode('utf-8')
4469 # callfunction=json.loads(request.args["callfunction"])
4470 try:
4471 TheJson=jsontransfert[session["sessionname"]]["TheJson"]
4472 except Exception as e:
4473 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
4474 return redirect(url_for(".editsession",message=message))
4475
4476 callfunction=json.loads(jsontransfert[session["sessionname"]]["callfunction"])
4477
4478 logging.debug("jsoneditor : "+str(callfunction))
4479 if ( request.method == 'POST'):
4480 message=callfunction["args"]
4481 TheJson=json.loads(request.form.get("submit"))
4482 OutJson=json.dumps(TheJson).replace("'", '"')
4483 #logging.debug("jsoneditor OutJson :"+OutJson)
4484 jsontransfert[session["sessionname"]]={"TheJson":TheJson}
4485 # message["TheJson"]=str(base64.b64encode(gzip.compress(OutJson.encode('utf-8'))))
4486 message=json.dumps(message) #.replace("'", '"')
4487 #logging.debug("jsoneditor message :"+str(message))
4488
4489 return redirect(url_for("."+callfunction["function"],message=message))
4490 return render_template("jsoneditor.html", **(myrender()), TheJson=TheJson)
4491
4492# ====================================================================
4493# Grid/main page
4494#cors = CORS(app, resources={r"/grid/*": {"origins": "*"}})
4495@app.route('/grid', methods=['GET', 'POST'])
4496@cross_origin()
4497def show_grid():
4498
4499 logging.warning("session in grid : "+str(session))
4500 #logging.debug("Enter in show_grid: with session "+str(session))
4501 if (not 'is_client_active' in session):
4502 flash("You are not connected. You must login before using a grid.")
4503 logging.error("You are not connected. You must login before using a grid."+str(session))
4504 return redirect("/login")
4505
4506 if request.method == 'POST' and "new room" in request.form :
4507 psession = request.form['new room']
4508 # TODO: properly close the old socket, or remove it from the room
4509 # (usefulness? it's only a testing tool at this point')
4510 logging.info("[!] Change session room to " + psession)
4511 session["sessionname"]=psession
4512 thesession = db.session.query(models.Sessions).filter_by(name=psession).scalar()
4513 if (type(thesession) != type(None)):
4514
4515 project = session["projectname"] = thesession.projects.name
4516
4517 else:
4518 logging.warning("You must choose a valid session")
4519 flash("You didn't select a valid session in grid.")
4520 return redirect("/allsessions")
4521 else: # GET
4522 if (session["is_client_active"]):
4523 try:
4524 cookieuser = {"username" : session["username"] }
4525 except:
4526 return redirect("/login")
4527
4528
4529 if (not "sessionname" in session or
4530 not "projectname" in session):
4531 flash("No project or session defined yet. Please chose one.")
4532 return redirect("/allsessions")
4533
4534 psession = session["sessionname"]
4535 project = session["projectname"]
4536 logging.info("Into '/grid' with GET for session room to " + psession)
4537
4538 try:
4539 part_nbr = str(len(room_dict[psession]))
4540 logging.debug(str(room_dict[psession]))
4541 except KeyError as e: # If the grid is the first to join the room, the "room_dict[session]" doesn't exist yet
4542 part_nbr = 0
4543 logging.info("first to join the room "+session["username"])
4544
4545 try:
4546 logging.debug("Grid with session :"+str(session["projectname"])+" "+str(session["sessionname"])+" "+str(session["username"]))
4547 except :
4548 pass
4549
4550 # Build Session
4551 ThisSession=db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
4552
4553 # Test if session is empty
4554 if (len(ThisSession.tile_sets) == 0):
4555 flash("No TileSet in this session : You must define tileset before going to grid.")
4556 message = '{"oldsessionname":"'+session["sessionname"]+'"}'
4557 return redirect(url_for(".editsession",message=message))
4558
4559
4560 # session["tilesetnames"]=[]
4561 # if (db.session.query(func.count(ThisSession.tile_sets)).scalar() > 0):
4562 if (type(ThisSession) != type(None)):
4563 ListAllTileSet_ThisSession=ThisSession.tile_sets
4564 else:
4565 logging.warning("You must choose a valid session")
4566 flash("You didn't select a valid session in grid.")
4567 return redirect("/allsessions")
4568 session["tilesetnames"]=[ thistileset.name for thistileset in ListAllTileSet_ThisSession ]
4569 logging.warning("All TileSet for session "+str(session["sessionname"])+" : "+str(session["tilesetnames"]))
4570
4571 # JsonSession={"info": {"SessionName" : sessionNAME,
4572 # "ProjectName" : ThisSession.projects.name,
4573 # "Users" : list(set([ThisSession.projects.users.name]+
4574 # [SessionUser.name for SessionUser in ThisSession.users]))},
4575 # "tilesets": [ {"name":thistileset.name,
4576 # "Dataset_path":thistileset.Dataset_path,
4577 # "tiles": [ {"id" : tile.id,
4578 # "title" : tile.title,
4579 # "comment": tile.comment,
4580 # "source": tile.source,
4581 # "tags": tile.tags
4582 # } for tile in thistileset.tiles ] }
4583 # for thistileset in ListAllTileSet_ThisSession ]
4584 # }
4585 # Need for saving session in a file ?
4586 # textSession=json.JSONEncoder().encode(JsonSession)
4587 #logging.debug(textSession)
4588
4589 # Main loop to build the grid :
4590 nbr_of_tiles=0
4591
4592 # If connection ? Test tileset connection ok ??
4593
4594 # (Temporary ?) build all tiles data vector
4595 global tiles_data
4596 tiles_data={}
4597 tiles_data["nodes"]=[]
4598 ts=0
4599 lts=len(ThisSession.tile_sets)
4600 while (ts < lts):
4601 thistileset=ThisSession.tile_sets[ts]
4602 nbtiles=len(thistileset.tiles)
4603 nbr_of_tiles = nbr_of_tiles + nbtiles
4604 if (nbtiles < 1):
4605 flash("Before grid, ERROR IN SESSION '%s' with TileSet '%s' : no tiles." % (ThisSession.name,thistileset.name))
4606 return redirect("/allsessions")
4607
4608 tiledata=tvdb.encode_tileset(thistileset)
4609 tiles_data["nodes"]=tiles_data["nodes"]+tiledata
4610 ts=ts+1
4611
4612 #logging.debug(str(tiles_data))
4613 session["nbr_of_tiles"]=nbr_of_tiles
4614 logging.info("nbr_of_tiles="+str(session["nbr_of_tiles"]))
4615
4616 config["nbr_of_tiles"] = nbr_of_tiles
4617
4618 # Global tags search
4619 Tags={}
4620 Tags["globalTags"]=[]
4621 Tags["FloatingTags"]={}
4622
4623 # df_nodes_normalized : DataFrame()
4624 # df_nodes_normalized contains the DataFrame of tiles_data["nodes"]
4625 df_nodes_normalized = pd.json_normalize(tiles_data["nodes"])
4626
4627 # df_column_tag_normalized : DataFrame()
4628 # df_column_tag_normalized contains the DataFrame of tags column of df_nodes_normalized DataFrame
4629 df_column_tag_normalized = df_nodes_normalized["tags"]
4630 print(df_column_tag_normalized)
4631 # tags_normalized : DataFrame()
4632 # tags_normalized contains the DataFrame of node/tile tags
4633 df_tags_normalized = pd.DataFrame()
4634
4635 # tag_lines : list(dict())
4636 # tag_lines contains tags information in a dictionary format
4637 tag_lines = []
4638 floating_tag={}
4639
4640 #i : int
4641 for i in range(0, len(df_column_tag_normalized)):
4642 # dict_line : dict()
4643 dict_line = dict()
4644 tags_node = []
4645
4646 # j : int
4647 for j in range(0, len(df_column_tag_normalized[i])):
4648
4649 newline = df_column_tag_normalized[i][j]
4650 newline = newline.replace("{", "")
4651 newline = newline.replace("}", "")
4652
4653 # list_line : []
4654 # list_line contains the list of elements of le string line
4655 list_line = newline.split(',')
4656
4657 # tag_name : str
4658 tag_name = list_line[0]
4659 tags_node.append(tag_name)
4660
4661 # if it's a variable tag
4662 if len(list_line) > 1:
4663 value_min = list_line[1]
4664 value = list_line[2]
4665 value_max = list_line[3]
4666 dict_line[tag_name] = float(value)
4667 floating_tag[tag_name]={'m':value_min,'M':value_max}
4668
4669 # if the tag is the last of the node/tile
4670 if j == len(df_column_tag_normalized[i]) -1 :
4671 tag_lines.append(dict_line)
4672 dict_line = {}
4673
4674 for tag in tags_node:
4675 if (not tag in Tags["globalTags"]):
4676 Tags["globalTags"].append(tag)
4677 if (tag in floating_tag):
4678 Tags["FloatingTags"][tag]=floating_tag[tag]
4679
4680
4681 tiles_data["config"] = config
4682 psgeom={}
4683 if (not session["is_client_active"]):
4684 try:
4685 psgeom=json.loads(session["geometry"])
4686 logging.warning("geom for passive client :"+str(psgeom)+" "+str(type(psgeom)))
4687 except:
4688 traceback.print_exc(file=sys.stderr)
4689
4690 TheConfig=ThisSession.config;
4691 if (ThisSession.config is None):
4692 flash("Session {} does not have a valid configuration for grid.".format(session["sessionname"]))
4693 message = '{"sessionname":"'+session["sessionname"]+'"}'
4694 return redirect(url_for(".configsession",message=message))
4695 # if (TheConfig==""):
4696 # config_default_file=open("app/static/js/config_default.json",'r')
4697 # json_configs=json.load(config_default_file)
4698 # config_default_file.close()
4699 # TheConfig=json.JSONEncoder().encode(json_configs)
4700 logging.debug("config : "+str(TheConfig))
4701
4702 try:
4703 lang=TheConfig["language"];
4704 except:
4705 lang="EN";
4706
4707 #get actions files for each TS/connection
4708 lts=len(ThisSession.tile_sets)
4709 tiles_actions={}
4710 ts=0
4711 while (ts < lts):
4712
4713 thistileset=ThisSession.tile_sets[ts]
4714 # Connection for this TS
4715 tsconnection=thistileset.connections
4716
4717 if (type(tsconnection) != type(None) and
4718 len(thistileset.config_files) > 0):
4719 if ("connection"+str(tsconnection.id) in session):
4720 asaction=False
4721 if ( "config.tar" in thistileset.config_files ):
4722 tar_config_file=tarfile.TarFile(name=thistileset.config_files["config.tar"],mode='r')
4723 tar_config_file.list()
4724 try:
4725 actions_file=tar_config_file.extractfile("actions.json")
4726 tiles_actions[thistileset.name]=json.loads(actions_file.read().decode('utf-8'))
4727 asaction=True
4728 except:
4729 pass
4730
4731 if ( "actions.json" in thistileset.config_files ):
4732 actions_file=open(thistileset.config_files["actions.json"],'r')
4733 tiles_actions[thistileset.name]=json.load(actions_file)
4734 asaction=True
4735 if (asaction):
4736 tiles_actions[thistileset.name]["action0"]=["get_new_nodes","system_update_alt"]
4737 # Search kill_all_containers action to register it in session cookie for this connection:
4738 if (session["is_client_active"]):
4739 rekillid=re.compile(r'"killid"')
4740 for actionid in tiles_actions[thistileset.name]:
4741 if (tiles_actions[thistileset.name][actionid][0] == "kill_all_containers" and
4742 not re.search(rekillid,session["connection"+str(tsconnection.id)])):
4743 session["connection"+str(tsconnection.id)]=session["connection"+str(tsconnection.id)].replace(', "vncpassword"',', "killid":"'+actionid.replace("action","")+'", "vncpassword"')
4744 else:
4745 tiles_actions[thistileset.name]={}
4746 else:
4747 tiles_actions[thistileset.name]={}
4748 ts=ts+1
4749
4750 if (len(tiles_actions)>0):
4751 logging.warning("Global tile actions : "+str(tiles_actions))
4752
4753 if ("colorTheme" in TheConfig["colors"]):
4754 colorTheme=TheConfig["colors"]["colorTheme"]
4755 logging.info("Color : "+str(colorTheme))
4756 else:
4757 colorTheme="dark"
4758 logging.info("Color always "+str(colorTheme))
4759 help_path="doc/user_doc_" + lang + "_" + colorTheme + ".html";
4760 logging.info("helpPath ="+str(help_path))
4761
4762 # Compute project role for template (owner only see invite button)
4763 try:
4764 current_user_obj = db.session.query(models.Users).filter_by(name=session["username"]).first()
4765 membership = None
4766 if current_user_obj and ThisSession:
4767 membership = db.session.query(models.ProjectMembers).filter_by(
4768 project_id=ThisSession.id_projects, user_id=current_user_obj.id
4769 ).first()
4770 project_role = membership.role_type if membership else None
4771 except Exception:
4772 project_role = None
4773
4774 return render_template("grid_template.html",
4775 user=session["username"],
4776 title="TiledViz on "+project,
4777 project=project,
4778 session=psession,
4779 description=str(ThisSession.description),
4780 json_geom=psgeom,
4781 participants=part_nbr,
4782 is_client_active=session["is_client_active"],
4783 project_role=project_role,
4784 json_data=tiles_data,
4785 json_actions=tiles_actions,
4786 json_config=TheConfig,
4787 json_Tags=Tags,
4788 helpPath = help_path)
4789
4790@socketio.on("save_Session")
4791def saveSession(cdata):
4792 croom=cdata["room"]
4793 logging.warning("[->] saveSession \"" + str(cdata["NewSuffix"]) + "\" with description \"" + str(cdata["NewDescription"]) + "\" in room " + str(croom))
4794 alltilesjson=json.loads(cdata["Session"].replace("'", '"'));
4795 save_session(str(session["sessionname"]),str(cdata["NewSuffix"]),str(cdata["NewDescription"]),alltilesjson)
4796
4797@socketio.on("share_Selection")
4798def shareSelection(cdata):
4799 croom=cdata["room"]
4800 listSelectionIds=json.loads(cdata["Selection"]);
4801 logfun("[->] shareSelection " + str(len(listSelectionIds)) + " nodes in room " + str(croom))
4802 sdata = {"Selection":cdata["Selection"]}
4803 socketio.emit('receive_deploy_Selection', sdata,room=croom)
4804
4805@socketio.on("deploy_Session")
4806def deploySession(cdata):
4807 croom=cdata["room"] # room is old session
4808 logging.warning("[->] deploySession NEW ROOM : '" + str(cdata["NewRoom"]) + "' from room " + str(croom))
4809 sdata = {"NewRoom":cdata["NewRoom"]}
4810 session["sessionname"]=cdata["NewRoom"];
4811 socketio.emit('receive_deploy_Session', sdata,room=croom) # change room for new session ?
4812
4813@socketio.on("share_Config")
4814def shareConfig(cdata):
4815 try:
4816 croom=cdata["room"]
4817 logging.warning("[->] shareConfig in room " + str(croom))
4818 configJson=json.loads(cdata["Config"].replace("'", '"'));
4819 logging.info("Config: "+str(configJson))
4820 oldsessionname=session["sessionname"]
4821 ThisSession = db.session.query(models.Sessions).filter(models.Sessions.name.like(oldsessionname)).first()
4822 oldconfig=ThisSession.config
4823 # Update modified config
4824 for section in configJson:
4825 thisConfigSection=configJson[section]
4826 for key in thisConfigSection:
4827 ThisSession.config[section][key]=thisConfigSection[key]
4828 flag_modified(ThisSession,"config")
4829 db.session.commit()
4830 sdata = {"Config":configJson};
4831 socketio.emit('receive_deploy_Config', sdata,room=croom)
4832 except Exception as e:
4833 logging.error("err : "+str(e))
4834 logging.error("cData : "+str(cdata))
4835 traceback.print_exc(file=sys.stderr)
4836
4837@socketio.on('move_tile')
4838def handle_click_event(cdata):
4839 global tiles_data
4840 croom = cdata["room"]
4841 logfun("[->] Click on tile " + str(cdata["id"]) + " in room " + str(croom))
4842 logging.info("[+] Position: (" + str(cdata["posX"]) + ", " + str(cdata["posY"])+ ")")
4843
4844 session_id = request.sid
4845 sdata = {"id":cdata["id"],"posX":cdata["posX"], "posY":cdata["posY"], "session_id":session_id}
4846
4847 socketio.emit('receive_move', sdata, room=croom )
4848 tileid=tiles_data["nodes"][int(cdata["id"])]["dbid"]
4849 logging.debug("move id = "+cdata["id"]+" db id = "+str(tileid))
4850 tile=db.session.query(models.Tiles).filter_by(id=tileid).one()
4851 logging.debug("title = "+tile.title)
4852 logging.debug("old pos = (%d,%d)" % (int(tile.pos_px_x),int(tile.pos_px_y)))
4853 tile.pos_px_x=int(cdata["posX"])
4854 tile.pos_px_y=int(cdata["posY"])
4855 logging.debug("new pos = (%d,%d)" % (int(tile.pos_px_x),int(tile.pos_px_y)))
4856 db.session.commit()
4857
4858 logging.info("[+] New position for tile " + cdata["id"] + " transmitted to " + str(len(room_dict[croom])) + " sockets")
4859 return [cdata["id"], 2, croom]
4860
4861
4862@socketio.on("click_Menu")
4863def MenuShare(cdata):
4864 croom=cdata["room"]
4865 logging.info("MenuShare :"+str(cdata))
4866 logfun("[->] Click on menu " + str(cdata["Menu"]) + " option icon " + str(cdata["optionButton"]) + " in room " + str(croom))
4867
4868 sdata = {"Menu":cdata["Menu"], "optionNumber":cdata["optionNumber"], "optionButton":cdata["optionButton"]}
4869 socketio.emit('receive_Menu_click', sdata,room=croom)
4870
4871@socketio.on("click")
4872def ClickShare(cdata):
4873 croom=cdata["room"]
4874 action=cdata["action"]
4875 logging.info(action+"Share :"+str(cdata))
4876 logfun("[->] Click on "+action+ " "+ str(cdata["id"]) + " in room " + str(croom))
4877 sdata = {"action":action,"id":cdata["id"]}
4878 socketio.emit('receive_click', sdata,room=croom)
4879
4880@socketio.on("click_val")
4881def ClickShareVal(cdata):
4882 croom=cdata["room"]
4883 action=cdata["action"]
4884 logging.info(action+"Share with val :"+str(cdata))
4885 logfun("[->] Click on "+action+ " "+ str(cdata["id"]) + " with val " + str(cdata["val"]) + " in room " + str(croom))
4886 sdata = {"action":action,"id":cdata["id"],"val":cdata["val"]}
4887 socketio.emit('receive_click_val', sdata,room=croom)
4888
4889@socketio.on("change_Opacity")
4890def changeOpacityShare(cdata):
4891 croom=cdata["room"]
4892 logging.info("changeOpacityShare :"+str(cdata))
4893 logfun("[->] Click on an opacity slider " + str(cdata["Id"]) + " in room " + str(croom))
4894
4895 sdata = {"Id":cdata["Id"],"Opacity":cdata["Opacity"]}
4896 socketio.emit('receive_Force_Opacity', sdata,room=croom)
4897
4898@socketio.on("add_Tag")
4899def addNewTagShare(cdata):
4900 croom=cdata["room"]
4901 logging.info("addNewTagShare :"+str(cdata))
4902 logfun("[->] Add a new tag " + str(cdata["NewTag"]) + " in room " + str(croom) + " from button " + str(cdata["option"]))
4903
4904 sdata = {"NewTag":cdata["NewTag"],"option":cdata["option"]}
4905 socketio.emit('receive_Add_Tag', sdata,room=croom)
4906
4907@socketio.on("switch_MultipleTag")
4908def switchMultipleTagShare(cdata):
4909 croom=cdata["room"]
4910 logging.info("MultipleTagsSelection :"+str(cdata))
4911 logfun("[->] Switch a multiple tags selection " + str(cdata["SelTags"]) + " in room " + str(croom) + " on/off " + str(cdata["bool"]))
4912
4913 sdata = {"SelTags":cdata["SelTags"],"bool":cdata["bool"]}
4914 socketio.emit('receive_multiple_Tags', sdata,room=croom)
4915
4916
4917@socketio.on("color_Tag")
4918def colorTagShare(cdata):
4919 croom=cdata["room"]
4920 logging.info("changeColorTagShare :"+str(cdata))
4921 logfun("[->] Change color for the tag " + str(cdata["OldTag"]) + " for " + str(cdata["TagColor"]) +" in room " + str(croom))
4922
4923 sdata = {"OldTag":cdata["OldTag"],"TagColor":cdata["TagColor"]}
4924 socketio.emit('receive_Color_Tag', sdata,room=croom)
4925
4926@socketio.on("action_click")
4927def ClickAction(cdata):
4928 if (session["is_client_active"]):
4929 croom=cdata["room"]
4930 action=cdata["action"]
4931 TS=cdata["TileSet"]
4932 selections=cdata["selections"]
4933 logging.info(action+" for TileSet "+TS)
4934 logfun("[->] Click on action "+action+ " "+ str(cdata["id"]) + " in room " + str(croom) + " for selection "+ str(selections))
4935
4936 actionid=int(action.replace("action", ""))
4937 command=str(actionid)+","+selections
4938
4939 oldtileset=db.session.query(models.TileSets).filter_by(name=TS).one()
4940 oldconnection=oldtileset.connections
4941 user_id=get_user_id("action_click",session["username"])
4942
4943 if (not oldconnection):
4944 logging.error("[->] NO Connection : "+str(oldconnection)+" on tileset "+str(oldtileset)+" for user "+str(user_id))
4945 return
4946
4947 if (user_id != oldconnection.id_users or not session["is_client_active"]):
4948 logging.error("[->] Connection id : "+str(oldconnection.id_users)+" for user "+str(user_id)+" and session active "+str(session["is_client_active"]))
4949 myflush()
4950 return
4951
4952 logfun("actionid %d" % (actionid))
4953 myflush()
4954 if (actionid == 0):
4955 ThisSession=db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
4956 # save old nodes.json before get new
4957 out_nodes_json = os.path.join("/TiledViz/TVFiles", str(oldconnection.id_users), str(oldconnection.id),"nodes.json")
4958 mvDATE=datetime.datetime.now().isoformat().replace(":","-")
4959 save_nodes_json=out_nodes_json+"_"+mvDATE
4960 logging.warning("action 0 : Save old nodes.json in %s" % (save_nodes_json))
4961 myflush()
4962 os.system("mv "+out_nodes_json+" "+save_nodes_json)
4963 #shutil.copyfile(out_nodes_json,save_nodes_json)
4964 # selection must be all tileset
4965 command=str(actionid)+","+","
4966 logfun("action command %s" % (command))
4967 myflush()
4968
4969 searchKillid=re.search(r'"killid":"\d+"',session["connection"+str(oldconnection.id)])
4970 killid=-1
4971 if (searchKillid):
4972 killid=int(searchKillid.group().replace('"killid":','').replace('"',''))
4973 if (actionid==killid):
4974 logging.warning("action %d : Find kill_all_containers action." % (actionid))
4975 command=str(actionid)+","+","
4976
4977 logging.warning("action: "
4978 +str(session["username"])+" ; "
4979 +str(oldtileset.id)+" ; "
4980 +str(oldconnection.id)+" ; "
4981 +str(command))
4982 myflush()
4983
4984 if (searchKillid):
4985 if (actionid==killid):
4986 time.sleep(timeAlive)
4987 logging.warning("action %d : Remove connection %d ." % (actionid,oldconnection.id))
4988 remove_this_connection(oldtileset,oldconnection.id,user_id)
4989
4990 if (actionid == 0):
4991 try:
4992 logging.warning("Update nodes for session %s" % (session["sessionname"]))
4993 myflush()
4994 ThisSession=db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
4995 # action0 == get new nodes.json file
4996 time.sleep(timeAlive)
4997
4998 # copy old nodes.json
4999 out_nodes_json = os.path.join("/TiledViz/TVFiles", str(oldconnection.id_users), str(oldconnection.id),"nodes.json")
5000 #diff save_nodes_json out_nodes_json ?
5001
5002 # Old tile set data
5003 with open(save_nodes_json) as save_json_tiles_file:
5004 tiledata1=json.loads(save_json_tiles_file.read())
5005 save_json_tiles_file.close()
5006
5007 # Add old index => For oldtiledset.tiles ??
5008 itiledata1={"nodes":[]};
5009 for idx, tile in enumerate(tiledata1["nodes"]):
5010 itiledata1["nodes"].append({"i":idx, "title":tile["title"]})
5011
5012 # New tile set data
5013 count_exist_new_nodes=0
5014 not_loaded=True
5015 while(not_loaded):
5016 try:
5017 time.sleep(timeAlive)
5018 with open(out_nodes_json) as json_tiles_file:
5019 tiledata2=json.loads(json_tiles_file.read())
5020 json_tiles_file.close()
5021 not_loaded=False
5022 except Exception as err:
5023 count_exist_new_nodes=count_exist_new_nodes+1
5024 NbIter=10
5025 if ( count_exist_new_nodes > NbIter):
5026 traceback.print_exc(file=sys.stderr)
5027 strerror=str(err)
5028 logging.error("After %d x %d s we still have this error :\n %s" % (NbIter, timeAlive,strerror))
5029 return
5030
5031 # sort Tiles data for old and new version of tileset
5032 sortdata1 = sorted(tiledata1["nodes"], key=lambda v: v["title"])
5033 isortdata1 = sorted(itiledata1["nodes"], key=lambda v: v["title"])
5034 sortdata2 = sorted(tiledata2["nodes"], key=lambda v: v["title"])
5035
5036 connectionbool=True
5037 urlbool=False
5038 datapath=""
5039 creation_date=datetime.datetime.now()
5040
5041 # DIFF sorted tiledata1 and tildedata2 and modify them in oldtiledset.tiles
5042 logging.warning("DIFF sorted tiledata1 and tildedata2 ")
5043 myflush()
5044 modtiles_data=[]
5045 for idx1, tile1 in enumerate(sortdata1):
5046 found=False
5047 for idx2, tile2 in enumerate(sortdata2):
5048 if (tile1 == tile2):
5049 del sortdata2[idx2]
5050 logging.debug("OK equal tiles %d %d " % (idx1, idx2))
5051 found=True
5052 break
5053 elif (tile1["url"] == tile2["url"]):
5054 logging.debug("equal url modify tiles %d %d " % (idx1, idx2))
5055 i=isortdata1[idx1]["i"]
5056
5057 title,name,comment,tags,variable,pos_px_x,pos_px_y,IdLocation,url,ConnectionPort = \
5058 convertTile(tile2,oldtileset.name,connectionbool,urlbool,datapath)
5059
5060 oldtileset.tiles[i].tags=tags
5061 oldtileset.tiles[i].source= {"name" : name,
5062 "connection" : ConnectionPort,
5063 "url" : url,
5064 "variable": variable}
5065 flag_modified(oldtileset.tiles[i],"source")
5066
5067 oldtileset.tiles[i].pos_px_x= pos_px_x
5068 oldtileset.tiles[i].pos_px_y= pos_px_y
5069 oldtileset.tiles[i].IdLocation=IdLocation
5070 db.session.commit()
5071 logging.debug("OK equal url modify tile")
5072
5073 modtiles_data.append((i,tile2))
5074
5075 del sortdata2[idx2]
5076 found=True
5077 break
5078
5079 # emit receive_deploy_nodes
5080 sdata = {"id":cdata["id"], "modtiles_data":modtiles_data}
5081 logging.debug("receive_deploy_nodes data : "+str(sdata))
5082 myflush()
5083
5084 socketio.emit('receive_deploy_nodes', sdata,room=croom)
5085 except Exception as err:
5086 traceback.print_exc(file=sys.stderr)
5087 strerror=str(err)
5088 logging.error(strerror)
5089
5090# Draw
5091sidDraw=""
5092@socketio.on("drawBlob")
5093def drawBlobShare(cdata):
5094 sidDraw = request.sid
5095 croom=cdata["room"]
5096 logging.info("drawBlobShare :"+str(cdata))
5097 logfun("[->] Share image from draws with blob " + str(cdata["nodeId"]) + " in room " + str(croom))
5098 nbsend=cdata["nbsend"]
5099 # start send draw to all clients
5100 for csid in room_dict[croom]:
5101 if ( not csid == sidDraw ):
5102 socketio.emit('receive_draw_img', cdata,room=croom+str(csid))
5103 logging.info("Send draw_img signal to client "+croom+str(csid))
5104
5105@socketio.on("uploadDraw")
5106def uploadDraw(cdata):
5107 croom=cdata["room"]
5108 #logging.info("Receive updloadDraw from client "+str(request.sid))
5109 for csid in room_dict[croom]:
5110 if ( not csid == sidDraw ):
5111 socketio.emit('receive_draw_part', cdata,room=croom+str(csid))
5112 #logging.info("Send draw part to client "+croom+str(csid)+" from "+str(cdata["offset"])+" to "+str(cdata["offsetEnd"]))
5113
5114@socketio.on("modif_draws")
5115def ModifDraws(cdata):
5116 croom=cdata["room"]
5117 action=cdata["action"]
5118 logging.debug(action+" Share :"+str(cdata))
5119 logfun("[->] Modification of draw from "+str(cdata["nodeId"])+ " with "+action + " in room " + str(croom))
5120 sdata = {"nodeId":cdata["nodeId"]}
5121 socketio.emit('receive_'+action, sdata,room=croom)
5122
5123# Connection
5124@socketio.on('connected_grid')#, namespace='/grid')
5125def config_client(cdata):
5126 logging.warning("[->] Socket connected to a grid")
5127 logging.info("[+] Project : " + str(cdata['project']))
5128 logging.info("[+] Session : " + str(cdata['session']))
5129
5130 logging.warning("Connect "+str(cdata["user"])+str(cdata["project"])+str(cdata["session"]))
5131 croom = cdata['session'] # c[lient]room
5132 session["projectname"]=cdata['project']
5133 session["sessionname"]=cdata['session']
5134 join_room(croom)
5135 clients.append(request.sid)
5136 logging.debug(rooms()) # rooms() list the rooms for the socket
5137 #if not(croom in room_dict):
5138 # room_dict.append(croom)
5139 # if room_dict.has_key(croom): # has_key is deprecated in python3
5140 if croom in room_dict:
5141 room_dict[croom].append(request.sid)
5142 logging.info("[+] " + croom + " : Now " + str(len(room_dict[croom])) + " sockets connected to the room")
5143 else:
5144 room_dict[croom] = [request.sid]
5145 if ("username" in session):
5146 logging.info("[+] " + croom + " : First client to join the room "+session["username"])
5147 else:
5148 logging.info("[+] " + croom + " : Anonymous client to join the room.")
5149 croomsid = cdata['session']+str(request.sid) # c[lient]room
5150 join_room(croomsid)
5151 room_dict[croomsid] = [request.sid]
5152 if ("username" in session):
5153 logging.info("[+] " + croomsid + " : Individual room "+session["username"])
5154 else:
5155 logging.info("[+] " + croomsid + " : Individual room for Anonymous client.")
5156
5157 clean_rooms()
5158 logging.info("room_dict are : "+ str(room_dict) + " my rooms "+ str(croom)+ " and "+str(croomsid))
5159
5160 try:
5161 sdata = {"part_nbr_update": str(len(room_dict[croom])-1)}
5162 socketio.emit("new_client", sdata, room=croom)
5163 except:
5164 traceback.print_exc(file=sys.stderr)
5165 logging.error("Unknown room %s id %d in room_dict %s " % (croom,croomsid,str(room_dict)))
5166 return {"room":croom, "session_id":request.sid}
5167
5168@socketio.on("disconnect")
5169def disconnect_socket():
5170 logging.debug("[<-] Socket disconnected")
5171 tmp_sid = request.sid
5172 for key in room_dict:
5173 if tmp_sid in room_dict[key]:
5174 room_dict[key].remove(tmp_sid)
5175 logging.info ("[-] " + key + " : Socket " + tmp_sid + " disconnected")
5176 sdata = {"part_nbr_update": str(len(room_dict[key])-1)}
5177 socketio.emit("new_client", sdata, room=key)
5178 return room_dict
5179
5180def clean_rooms():
5181 try:
5182 for key in room_dict:
5183 if (not room_dict[key]):
5184 room_dict.pop(key)
5185 except:
5186 time.sleep(2)
5187 clean_rooms()
5188
5189@socketio.on("get_link")
5190def handle_invite_link_request(cdata):
5191 croom = cdata["session"]
5192 is_new_client_active = cdata["type"]
5193 max_uses = cdata.get("max_uses", 1)
5194 invitee_name = cdata.get("invitee_name", "")
5195
5196 # SECURITY CHECK: Verify user is logged in and has permission to create invites
5197 if "username" not in session or session["username"] == "Anonymous":
5198 socketio.emit("get_link_back", {"error": "You must be logged in to create invitation links"}, room=croom)
5199 return
5200
5201 # SECURITY CHECK: Verify user is a member of the project
5202 if "projectname" not in session or "sessionname" not in session:
5203 socketio.emit("get_link_back", {"error": "Invalid session context"}, room=croom)
5204 return
5205
5206 # Get current user
5207 current_user = db.session.query(models.Users).filter_by(name=session["username"]).first()
5208 if not current_user:
5209 socketio.emit("get_link_back", {"error": "User not found"}, room=croom)
5210 return
5211
5212 # Get session and project
5213 session_obj = db.session.query(models.Sessions).filter(models.Sessions.name.like(session["sessionname"])).first()
5214 if not session_obj:
5215 socketio.emit("get_link_back", {"error": "Session not found"}, room=croom)
5216 return
5217
5218 project_id = session_obj.id_projects
5219 if not project_id:
5220 socketio.emit("get_link_back", {"error": "Session has no associated project"}, room=croom)
5221 return
5222
5223 # SECURITY CHECK: Verify user is a project member with appropriate permissions
5224 user_membership = db.session.query(models.ProjectMembers).filter_by(
5225 project_id=project_id,
5226 user_id=current_user.id
5227 ).first()
5228
5229 if not user_membership:
5230 socketio.emit("get_link_back", {"error": "You must be a project member to create invitation links"}, room=croom)
5231 return
5232
5233 # SECURITY CHECK: Only owners can create invites
5234 if user_membership.role_type not in valid_manage_members:
5235 socketio.emit("get_link_back", {"error": f"Insufficient permissions. Your role '{user_membership.role_type}' cannot create invitation links"}, room=croom)
5236 return
5237
5238 # Validate max_uses
5239 try:
5240 max_uses = int(max_uses)
5241 if max_uses < 1:
5242 socketio.emit("get_link_back", {"error": "Maximum uses must be a positive number"}, room=croom)
5243 return
5244 except (ValueError, TypeError):
5245 socketio.emit("get_link_back", {"error": "Invalid maximum uses value"}, room=croom)
5246 return
5247
5248 # For active links, validate that the invitee exists
5249 if is_new_client_active:
5250 if not invitee_name:
5251 socketio.emit("get_link_back", {"error": "Invitee name is required for active links"}, room=croom)
5252 return
5253
5254 # Check if the invitee exists in the database
5255 invitee = db.session.query(models.Users).filter_by(name=invitee_name).first()
5256 if not invitee:
5257 socketio.emit("get_link_back", {"error": f"User {invitee_name} does not exist"}, room=croom)
5258 return
5259
5260 if (is_new_client_active):
5261 client_type = "active"
5262 else:
5263 client_type = "passive"
5264
5265 try:
5266 DEFAULT_URL=os.getenv("SERVER_NAME")+"."+os.getenv("DOMAIN")
5267 print(DEFAULT_URL)
5268 except:
5269 DEFAULT_URL = "0.0.0.0:5000"
5270
5271 creation_date = datetime.datetime.now().isoformat()
5272
5273 # Generate a unique key for the invitation
5274 key = linkrandom(32)
5275 try:
5276 key = key.decode('utf-8')
5277 except AttributeError:
5278 pass # key est déjà une str
5279
5280 # Créer la clé du lien (à stocker dans la base)
5281 if (client_type == "active"):
5282 link_key = f"{session['sessionname']}{linkChar}{client_type}{linkChar}{invitee_name}{linkChar}{creation_date}{linkChar}{key}"
5283 else:
5284 link_key = f"{session['sessionname']}{linkChar}{client_type}{linkChar}Anonymous{linkChar}{creation_date}{linkChar}{key}"
5285
5286 # Générer le lien complet à afficher à l'utilisateur
5287 sdata = {
5288 "link": f"https://{DEFAULT_URL}/join/{link_key}",
5289 "max_uses": max_uses
5290 }
5291
5292 # Store ONLY the key in the database
5293 try:
5294 ErrLink="Full link %s with session %s " % (sdata["link"],session["sessionname"])
5295 flash(ErrLink)
5296 logging.warning(ErrLink)
5297 invite_link = models.InviteLinks(
5298 link=link_key,
5299 host_user=session["username"],
5300 host_project=session["projectname"],
5301 type=is_new_client_active,
5302 creation_date=datetime.datetime.now(),
5303 max_uses=max_uses,
5304 use_count=0,
5305 id_sessions=db.session.query(models.Sessions.id).filter(models.Sessions.name.like(session["sessionname"])).scalar(),
5306 id_users=invitee.id if is_new_client_active else None
5307 )
5308 db.session.add(invite_link)
5309 db.session.commit()
5310 except Exception as e:
5311 logging.error(f"Error storing invite link: {str(e)}")
5312 db.session.rollback()
5313 socketio.emit("get_link_back", {"error": "Failed to create invitation link"}, room=croom)
5314 return
5315
5316 socketio.emit("get_link_back" ,sdata,room=croom)
5317
5318@app.route("/join/<link>")
5319def handle_join_with_invite_link(link):
5320 logging.warning("Handle join with invite link : " + link)
5321
5322 # Parse the link components
5323 link_parts = link.split(linkChar)
5324 if len(link_parts) < 5:
5325 flash("Invalid invitation link : wrong parts number %d in your link." % (len(link_parts)))
5326 return redirect(url_for(".index"))
5327
5328 session_name = link_parts[0]
5329 new_client_type = link_parts[1]
5330 username = link_parts[2]
5331 creation_date = link_parts[3]
5332 key = link_parts[4]
5333 hasgeom=False
5334 if len(link_parts) == 6:
5335 geom = link_parts[5]
5336 hasgeom=True
5337 logging.warning("Link with geom : %s " % (geom))
5338 link=session_name+linkChar+new_client_type+linkChar+username+linkChar+creation_date+linkChar+key
5339
5340 # Check if the invitation exists and is valid
5341 invite_link = db.session.query(models.InviteLinks).filter_by(link=link).first()
5342 if not invite_link:
5343 flash("Invalid invitation link : not found in DB")
5344 return redirect(url_for(".index"))
5345
5346 # Check if the invitation has reached its maximum number of uses
5347 if invite_link.use_count >= invite_link.max_uses:
5348 flash("This invitation has reached its maximum number of uses")
5349 delelement(models.InviteLinks, "link "+link, invite_link.id)
5350 db.session.commit()
5351 return redirect(url_for(".index"))
5352
5353 if (datetime.datetime.now() > invite_link.creation_date+datetime.timedelta(seconds=LinkExpiredAfterSec)):
5354 #datetime.datetime.strptime(invite_link.creation_date,'%Y-%m-%d %H:%M:%S.%f')
5355 flash("This invitation created at %s is older than expiration time after %d minutes" % (invite_link.creation_date,LinkExpiredAfterSec/60) )
5356 delelement(models.InviteLinks, "link "+link, invite_link.id)
5357 db.session.commit()
5358 return redirect(url_for(".index"))
5359
5360 # For active links, check if the user is logged in and matches the invitee
5361 user = None
5362 if new_client_type == "active":
5363 if "username" not in session:
5364 # Store the link in session for after login
5365 session["pending_invite_link"] = link
5366 flash("You must be logged in to use this invitation link")
5367 return redirect(url_for(".login"))
5368
5369 # Verify that the logged-in user matches the invitee
5370 if session["username"] != username:
5371 flash("This invitation link is for a different user")
5372 return redirect(url_for(".index"))
5373
5374 # Verify that the user exists in the database
5375 user = db.session.query(models.Users).filter_by(name=username).first()
5376 if not user:
5377 flash("Invalid user account : user not found")
5378 return redirect(url_for(".index"))
5379
5380 # Increment the use count
5381 invite_link.use_count += 1
5382 db.session.commit()
5383
5384 # Set session variables
5385 session["sessionname"] = session_name
5386 session["is_client_active"] = (new_client_type == "active")
5387 session["username"] = username
5388
5389 # Get session details
5390 ThisSession = db.session.query(models.Sessions).filter_by(name=session_name).first()
5391 if not ThisSession:
5392 flash_mess="Anonymous connection with unknown session : "+str(session["sessionname"])
5393 logging.error(flash_mess)
5394 flash(flash_mess+". Please check your command line")
5395 return redirect(url_for(".index"))
5396
5397 try:
5398 session["projectname"]=ThisSession.projects.name
5399 project_id = ThisSession.projects.id
5400 except:
5401 ErrLink="Error in link %s with project of session %s " % (link,ThisSession)
5402 flash(ErrLink)
5403 logging.error(ErrLink)
5404 return redirect(url_for(".index"))
5405
5406 # Synchronize user to project and session if it's an active link
5407 if new_client_type == "active" and user:
5408 sync_success, sync_message = sync_user_to_project_and_session(
5409 user_id=user.id,
5410 project_id=project_id,
5411 session_id=ThisSession.id,
5412 role_type='guest' # Default role for invited users
5413 )
5414 if not sync_success:
5415 logging.warning(f"Failed to synchronize user {user.name}: {sync_message}")
5416 flash(f"Warning: {sync_message}")
5417 else:
5418 logging.info(f"Successfully synchronized user {user.name} to project and session")
5419
5420 session["geometry"]='{}'
5421 if "passive" in link:
5422 if (hasgeom):
5423 my_geom=geom.replace('{','{"').replace(',',',"').replace('=','":')
5424 logging.warning("str json my_geom "+str(my_geom))
5425 session["geometry"]=my_geom
5426
5427 # Check if the invitation has reached its maximum number of uses and will be deleted
5428 if invite_link.use_count >= invite_link.max_uses:
5429 delelement(models.InviteLinks, "link "+link, invite_link.id)
5430 db.session.commit()
5431 logging.warning("session after join before grid : "+str(session))
5432 return redirect('/grid')
5433 #return(redirect("/grid", project=room))
5434 #return ("pouet")
5435
5436# ====================================================================
5437# Error management
5438@app.errorhandler(400)
5439def bad_request(e):
5440 logging.error("Bad request or cookie expired for user "+str(session["username"]))
5441 return render_template('error_pages/400.html'), 400
5442
5443@app.errorhandler(502)
5444def bad_req(e):
5445 logging.error("Cookie expired for user "+str(session["username"]))
5446 return render_template('error_pages/502.html'), 502
5447
5448@app.errorhandler(504)
5449def bad_connect(e):
5450 logging.error("Error of TVSecure for user "+str(session["username"]))
5451 return render_template('error_pages/504.html'), 504
5452
5453
5454@app.errorhandler(404)
5455def not_found(e):
5456 return render_template('error_pages/404.html'), 404
5457
5458@app.errorhandler(429)
5459def not_found(e):
5460 return render_template('error_pages/429.html'), 429
5461
5462@app.errorhandler(500)
5463def TVerror(e):
5464 logging.error("Error with TiledViz for user "+str(session["username"]))
5465 return render_template('error_pages/500.html'), 500
5466
5467# # Proxy VNC
5468# # Thank's to https://stackoverflow.com/posts/36601467/revisions
5469# @app.route('/<path:dummy>')
5470# def routevnc(path=None,dummy=None):
5471# is_noVNC=re.search(r''+"noVNC",dummy)
5472# is_images=re.search(r''+"(images|favicon.ico)",dummy)
5473# if ( is_images ):
5474# logging.warning("is_images : \n "+str(is_images))
5475# #dummy == "favicon.ico"
5476# logging.warning("proxy favicon : \n "+str(request.host_url))
5477# resp = requests.request(
5478# method=request.method,
5479# url=request.host_url,
5480# headers={key: value for (key, value) in request.headers if key != 'Host'},
5481# data=request.get_data(),
5482# cookies=request.cookies,
5483# allow_redirects=False)
5484# elif (is_noVNC) :
5485# #logging.debug("request : \n "+str(request))
5486# logging.debug("proxy noVNC path : \n "+str(path)+":"+str(dummy))
5487# logging.debug("routevnc : \n url "+str(request.host_url))
5488# # logging.error("routevnc : \n url "+str(request.host_url)+"\n method "+str(request.method).replace("\r","")+"\n header"+str(request.headers).replace("\r","")+"\n args :"+str(request.args).replace("\r",""))
5489
5490# VNCurl="https://"+app.config["SERVER_NAME"]
5491# logfun("Connect with url : "+VNCurl+dummy)
5492# newurl=request.url.replace(request.host_url, VNCurl)
5493# logfun("Replace url : "+newurl)
5494
5495# logging.debug(dummy+" header :"+str(request.headers))
5496# resp = requests.request(
5497# method=request.method,
5498# url=newurl,
5499# headers={key: value for (key, value) in request.headers if key != 'Host'},
5500# data=request.get_data(),
5501# cookies=request.cookies,
5502# allow_redirects=False)
5503# # elif (is_images) :
5504# # logging.warning("images request : \n "+str(request))
5505# # logging.warning("proxy images path : \n "+str(path)+":"+str(dummy))
5506# # logging.warning("host_url : \n url "+str(request.host_url))
5507# # # logging.error("routevnc : \n url "+str(request.host_url)+"\n method "+str(request.method).replace("\r","")+"\n header"+str(request.headers).replace("\r","")+"\n args :"+str(request.args).replace("\r",""))
5508
5509# # LocalUrl="https://localhost:5000/"
5510# # logfun("Connect with url : "+LocalUrl+dummy)
5511# # newurl=request.url.replace(request.host_url, LocalUrl)
5512# # logfun("Replace url : "+newurl)
5513
5514# # logging.warning(dummy+" header :"+str(request.headers))
5515# # resp = requests.request(
5516# # method=request.method,
5517# # url=newurl,
5518# # headers={key: value for (key, value) in request.headers if key != 'Host'},
5519# # data=request.get_data(),
5520# # cookies=request.cookies,
5521# # allow_redirects=False)
5522# else:
5523# logging.warning("proxy unknwon path : \n "+str(path)+":"+str(dummy)+" "+str(request.host_url))
5524# resp = requests.request(
5525# method=request.method,
5526# url=request.host_url,
5527# headers={key: value for (key, value) in request.headers if key != 'Host'},
5528# data=request.get_data(),
5529# cookies=request.cookies,
5530# allow_redirects=False)
5531
5532# excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
5533# headers = [(name, value) for (name, value) in resp.raw.headers.items()
5534# if name.lower() not in excluded_headers]
5535
5536# response = Response(resp.content, resp.status_code, headers)
5537# return response
5538
5539@app.route("/revoke_invite/<int:invite_id>", methods=["POST"])
5540def revoke_invite(invite_id):
5541 if "username" not in session:
5542 return jsonify({"error": "User not logged in"}), 401
5543
5544 invite_link = db.session.query(models.InviteLinks).filter_by(id=invite_id).first()
5545 if not invite_link:
5546 return jsonify({"error": "Invitation not found"}), 404
5547
5548 # Check if the user has permission to revoke this invitation
5549 if invite_link.host_user != session["username"]:
5550 return jsonify({"error": "Not authorized to revoke this invitation"}), 403
5551
5552 invite_link.is_revoked = True
5553 db.session.commit()
5554
5555 return jsonify({"message": "Invitation revoked successfully"})
5556
5557# Manage project members
5558@app.route('/project_members/<int:project_id>/members', methods=["GET", "POST"])
5559def project_members(project_id):
5560 if ("username" not in session or session["username"] == "Anonymous"):
5561 return redirect("/login")
5562
5563 user_id = get_user_id("Project Members", session["username"])
5564
5565 # Check if project exists
5566 project = db.session.query(models.Projects).filter_by(id=project_id).first()
5567 if not project:
5568 flash("Project not found!")
5569 return redirect("/project")
5570
5571 # Check if user has permission to manage this project (owner)
5572 if not can_manage_project(project_id, user_id):
5573 flash("You don't have permission to manage members for this project!")
5574 return redirect("/project")
5575
5576 # Get all current project members using utility function
5577 current_members = get_project_members(project_id)
5578
5579 # Get all available users for adding using utility function
5580 available_users = get_available_users_for_project(project_id)
5581
5582 # Handle form submissions
5583 if request.method == "POST":
5584 if 'add_member' in request.form:
5585 new_user_id = request.form.get('user_id')
5586 new_role = request.form.get('role_type')
5587
5588 if new_user_id and new_role:
5589 success, message = add_project_member(project_id, int(new_user_id), new_role)
5590 flash(message)
5591 if success:
5592 # Refresh the page to show updated member list
5593 return redirect(url_for('project_members', project_id=project_id))
5594
5595 elif 'remove_member' in request.form:
5596 member_id = request.form.get('member_id')
5597 if member_id:
5598 success, message = remove_project_member(project_id, int(member_id))
5599 flash(message)
5600 if success:
5601 # Refresh the page to show updated member list
5602 return redirect(url_for('project_members', project_id=project_id))
5603
5604 elif 'change_role' in request.form:
5605 member_id = request.form.get('member_id')
5606 new_role = request.form.get('new_role')
5607
5608 if member_id and new_role:
5609 success, message = update_member_role(project_id, int(member_id), new_role)
5610 flash(message)
5611 if success:
5612 # Refresh the page to show updated member list
5613 return redirect(url_for('project_members', project_id=project_id))
5614
5615 elif 'transfer_ownership' in request.form:
5616 new_owner_id = request.form.get('new_owner_id')
5617 if new_owner_id:
5618 success, message = transfer_project_ownership(project_id, user_id, int(new_owner_id))
5619 flash(message)
5620 if success:
5621 # Refresh the page to show updated member list
5622 return redirect(url_for('project_members', project_id=project_id))
5623
5624 # Redirect to refresh the page
5625 return redirect(url_for('project_members', project_id=project_id))
5626
5627 # Get updated member list for display
5628 current_members = get_project_members(project_id)
5629
5630 return render_template(
5631 "project_members.html",
5632 project=project,
5633 username=session["username"],
5634 current_members=current_members,
5635 available_users=available_users,
5636 title=f"Manage Members - {project.name}"
5637 )
5638
5639# API endpoint for ownership transfer with comprehensive validation
5640@app.route('/api/project/<int:project_id>/transfer-ownership', methods=['POST'])
5641def transfer_ownership_api(project_id):
5642 """
5643 API endpoint for transferring project ownership with comprehensive validation
5644 """
5645 if "username" not in session or session["username"] == "Anonymous":
5646 return jsonify({"error": "Authentication required"}), 401
5647
5648 user_id = get_user_id("Transfer Ownership", session["username"])
5649
5650 try:
5651 data = request.get_json()
5652 if not data or 'new_owner_id' not in data:
5653 return jsonify({"error": "new_owner_id is required"}), 400
5654
5655 new_owner_id = data['new_owner_id']
5656
5657 # Use utility function for ownership transfer
5658 success, message = transfer_project_ownership(project_id, user_id, new_owner_id)
5659
5660 if success:
5661 # Get new owner details for response
5662 new_owner = db.session.query(models.Users).filter_by(id=new_owner_id).first()
5663 return jsonify({
5664 "message": message,
5665 "new_owner": {
5666 "id": new_owner.id,
5667 "name": new_owner.name
5668 },
5669 "previous_owner": {
5670 "id": user_id,
5671 "name": session["username"]
5672 }
5673 }), 200
5674 else:
5675 return jsonify({"error": message}), 400
5676
5677 except Exception as e:
5678 db.session.rollback()
5679 logging.error(f"Error in ownership transfer API: {str(e)}")
5680 return jsonify({"error": "Internal server error during ownership transfer"}), 500
5681
5682# API endpoint to check ownership constraints
5683@app.route('/api/project/<int:project_id>/ownership-constraints', methods=['GET'])
5684def check_ownership_constraints(project_id):
5685 """
5686 API endpoint to check ownership constraints and business rules
5687 """
5688 if "username" not in session or session["username"] == "Anonymous":
5689 return jsonify({"error": "Authentication required"}), 401
5690
5691 user_id = get_user_id("Check Ownership Constraints", session["username"])
5692
5693 try:
5694 # Validate project exists
5695 project = db.session.query(models.Projects).filter_by(id=project_id).first()
5696 if not project:
5697 return jsonify({"error": "Project not found"}), 404
5698
5699 # Get current user's membership
5700 user_membership = db.session.query(models.ProjectMembers).filter_by(
5701 project_id=project_id,
5702 user_id=user_id
5703 ).first()
5704
5705 if not user_membership:
5706 return jsonify({"error": "User is not a member of this project"}), 403
5707
5708 # Count owners
5709 owner_count = db.session.query(models.ProjectMembers).filter_by(
5710 project_id=project_id,
5711 role_type='owner'
5712 ).count()
5713
5714 # Get all members for ownership transfer options
5715 all_members = db.session.query(models.ProjectMembers).filter_by(
5716 project_id=project_id
5717 ).all()
5718
5719 # Filter members who can receive ownership (not already owners)
5720 eligible_members = []
5721 for member in all_members:
5722 if member.role_type != 'owner' and member.user_id != user_id:
5723 user_info = db.session.query(models.Users).filter_by(id=member.user_id).first()
5724 if user_info:
5725 eligible_members.append({
5726 "id": member.user_id,
5727 "name": user_info.name,
5728 "role": member.role_type
5729 })
5730
5731 return jsonify({
5732 "is_owner": user_membership.role_type == 'owner',
5733 "owner_count": owner_count,
5734 "is_last_owner": owner_count <= 1 and user_membership.role_type == 'owner',
5735 "can_transfer_ownership": user_membership.role_type == 'owner' and len(eligible_members) > 0,
5736 "eligible_for_ownership_transfer": eligible_members,
5737 "constraints": {
5738 "cannot_remove_last_owner": owner_count <= 1,
5739 "must_be_member_to_receive_ownership": True,
5740 "cannot_transfer_to_self": True,
5741 "cannot_transfer_to_existing_owner": True
5742 }
5743 }), 200
5744
5745 except Exception as e:
5746 logging.error(f"Error checking ownership constraints: {str(e)}")
5747 return jsonify({"error": "Internal server error"}), 500
5748
5749# Display user projects with management options
5750@app.route('/my-projects', methods=["GET"])
5751def my_projects():
5752 if ("username" not in session or session["username"] == "Anonymous"):
5753 return redirect("/login")
5754
5755 user_id = get_user_id("My Projects", session["username"])
5756
5757 # Get all user projects using utility function
5758 user_projects = get_user_projects(user_id)
5759
5760 # Organize projects by role
5761 projects_by_role = {
5762 'owned': [],
5763 'member': []
5764 }
5765
5766 # Categorize projects by role
5767 for project, role_type in user_projects:
5768 project_data = {
5769 'project': project,
5770 'role': role_type,
5771 'can_manage': role_type in valid_manage_members
5772 }
5773
5774 if role_type == 'owner':
5775 projects_by_role['owned'].append(project_data)
5776 else:
5777 projects_by_role['member'].append(project_data)
5778
5779 return render_template(
5780 "my_projects.html",
5781 projects_by_role=projects_by_role,
5782 username=session["username"],
5783 title="My Projects"
5784 )
5785
5786
5787# Migration management routes
5788@app.route('/admin/migration', methods=['GET', 'POST'])
5789def migration_admin():
5790 """Admin interface for managing database migrations"""
5791 if ("username" not in session or session["username"] == "Anonymous"):
5792 return redirect("/login")
5793
5794 user_id = get_user_id("Migration Admin", session["username"])
5795 User = db.session.query(models.Users).filter_by(name=session["username"]).one()
5796
5797 if not User.is_admin:
5798 flash("You must be an administrator to access this page.")
5799 return redirect("/")
5800
5801 from app.migration import migrate_project_owners, check_migration_status, validate_migration, rollback_migration, get_orphaned_projects
5802
5803 # Handle migration actions
5804 if request.method == "POST":
5805 action = request.form.get('action')
5806
5807 if action == 'run_migration':
5808 if migrate_project_owners():
5809 flash("Migration completed successfully!")
5810 else:
5811 flash("Migration failed! Check logs for details.")
5812
5813 elif action == 'validate_migration':
5814 validation = validate_migration()
5815 if validation and validation['is_valid']:
5816 flash("Migration validation passed - data is consistent!")
5817 else:
5818 flash(f"Migration validation failed: {validation}")
5819
5820 elif action == 'rollback_migration':
5821 if rollback_migration():
5822 flash("Migration rollback completed!")
5823 else:
5824 flash("Migration rollback failed! Check logs for details.")
5825
5826 # Get current status
5827 status = check_migration_status()
5828 validation = validate_migration()
5829 orphaned_projects = get_orphaned_projects()
5830
5831 return render_template(
5832 "migration_admin.html",
5833 status=status,
5834 validation=validation,
5835 orphaned_projects=orphaned_projects,
5836 title="Migration Management"
5837 )
5838
5839@app.route('/admin/consistency', methods=['GET', 'POST'])
5840def consistency_admin():
5841 """
5842 Admin interface for session-project consistency management
5843 """
5844 if ("username" not in session or session["username"] == "Anonymous"):
5845 return redirect("/login")
5846
5847 user_id = get_user_id("Consistency Admin", session["username"])
5848 User = db.session.query(models.Users).filter_by(name=session["username"]).one()
5849
5850 if not User.is_admin:
5851 flash("You must be an administrator to access this page.")
5852 return redirect("/")
5853
5854 sessions_data = []
5855 if request.method == 'GET':
5856 # Get all sessions with their consistency status
5857 sessions = db.session.query(models.Sessions).all()
5858 for session_obj in sessions:
5859 inconsistencies = validate_session_project_consistency(session_obj.id)
5860 sessions_data.append({
5861 'id': session_obj.id,
5862 'name': session_obj.name,
5863 'project_name': session_obj.projects.name if session_obj.projects else 'No Project',
5864 'inconsistencies': inconsistencies,
5865 'is_consistent': len(inconsistencies) == 0
5866 })
5867
5868 if request.method == 'POST':
5869 action = request.form.get('action')
5870 session_id = request.form.get('session_id')
5871
5872 if action == 'validate_session' and session_id:
5873 inconsistencies = validate_session_project_consistency(int(session_id))
5874 if inconsistencies:
5875 flash(f"Found {len(inconsistencies)} inconsistencies in session {session_id}")
5876 for inc in inconsistencies:
5877 flash(inc)
5878 else:
5879 flash(f"Session {session_id} is consistent with its project")
5880
5881 elif action == 'fix_session' and session_id:
5882 success, message = fix_session_project_inconsistencies(int(session_id))
5883 if success:
5884 flash(message)
5885 else:
5886 flash(f"Error: {message}")
5887
5888 elif action == 'validate_all':
5889 # Validate all sessions
5890 sessions = db.session.query(models.Sessions).all()
5891 total_inconsistencies = 0
5892 inconsistent_sessions = 0
5893
5894 for session_obj in sessions:
5895 inconsistencies = validate_session_project_consistency(session_obj.id)
5896 if inconsistencies:
5897 inconsistent_sessions += 1
5898 total_inconsistencies += len(inconsistencies)
5899 flash(f"Session '{session_obj.name}' (ID: {session_obj.id}): {len(inconsistencies)} inconsistencies")
5900 for inc in inconsistencies:
5901 flash(f" - {inc}")
5902
5903 if total_inconsistencies == 0:
5904 flash("All sessions are consistent with their projects!")
5905 else:
5906 flash(f"Found {total_inconsistencies} inconsistencies across {inconsistent_sessions} sessions")
5907
5908 elif action == 'fix_all':
5909 # Fix all inconsistencies
5910 sessions = db.session.query(models.Sessions).all()
5911 total_fixed = 0
5912 fixed_sessions = 0
5913
5914 for session_obj in sessions:
5915 success, message = fix_session_project_inconsistencies(session_obj.id)
5916 if success and "Fixed" in message:
5917 # Extract number from message
5918 import re
5919 match = re.search(r'Fixed (\d+)', message)
5920 if match:
5921 fixed_count = int(match.group(1))
5922 total_fixed += fixed_count
5923 if fixed_count > 0:
5924 fixed_sessions += 1
5925 flash(f"Session '{session_obj.name}': {message}")
5926
5927 if total_fixed > 0:
5928 flash(f"Successfully fixed {total_fixed} inconsistencies across {fixed_sessions} sessions")
5929 else:
5930 flash("No inconsistencies found to fix")
5931
5932 return render_template('admin_consistency.html',
5933 title="Consistency Admin",
5934 sessions=sessions_data,
5935 **myrender())
5936
5937
5938@app.route('/project/<int:project_id>/invite', methods=['GET', 'POST'])
5939def project_invite(project_id):
5940 """
5941 Project invitation management interface
5942 """
5943 if ("username" not in session or session["username"] == "Anonymous"):
5944 return redirect("/login")
5945
5946 # Get current user
5947 current_user = db.session.query(models.Users).filter_by(name=session["username"]).first()
5948 if not current_user:
5949 flash("User not found")
5950 return redirect("/")
5951
5952 # Check if user can create invites for this project (owner only)
5953 can_create, reason = can_create_invite_links(current_user.id, project_id)
5954 if not can_create:
5955 flash(f"Access denied: {reason}")
5956 return redirect("/")
5957
5958 # Get project details
5959 project = db.session.query(models.Projects).filter_by(id=project_id).first()
5960 if not project:
5961 flash("Project not found")
5962 return redirect("/")
5963
5964 # Get project sessions
5965 sessions = db.session.query(models.Sessions).filter_by(id_projects=project_id).all()
5966
5967 # Get existing invite links for this project
5968 invite_links = db.session.query(models.InviteLinks).filter_by(
5969 host_project=project.name
5970 ).order_by(models.InviteLinks.creation_date.desc()).all()
5971
5972 if request.method == 'POST':
5973 action = request.form.get('action')
5974
5975 if action == 'create_invite':
5976 session_id = request.form.get('session_id')
5977 invitee_name = request.form.get('invitee_name', '').strip()
5978 max_uses = request.form.get('max_uses', 1)
5979 invite_type = request.form.get('invite_type', 'passive')
5980
5981 try:
5982 max_uses = int(max_uses)
5983 if max_uses < 1:
5984 flash("Maximum uses must be a positive number")
5985 return redirect(url_for('.project_invite', project_id=project_id))
5986 except (ValueError, TypeError):
5987 flash("Invalid maximum uses value")
5988 return redirect(url_for('.project_invite', project_id=project_id))
5989
5990 # Validate session belongs to project
5991 session_obj = db.session.query(models.Sessions).filter_by(
5992 id=session_id,
5993 id_projects=project_id
5994 ).first()
5995 if not session_obj:
5996 flash("Invalid session for this project")
5997 return redirect(url_for('.project_invite', project_id=project_id))
5998
5999 # For active invites, validate invitee exists
6000 if invite_type == 'active':
6001 if not invitee_name:
6002 flash("Invitee name is required for active invitations")
6003 return redirect(url_for('.project_invite', project_id=project_id))
6004
6005 invitee = db.session.query(models.Users).filter_by(name=invitee_name).first()
6006 if not invitee:
6007 flash(f"User '{invitee_name}' does not exist")
6008 return redirect(url_for('.project_invite', project_id=project_id))
6009 else:
6010 invitee_name = "Anonymous"
6011
6012 # Create invitation link
6013 creation_date = datetime.datetime.now().isoformat()
6014 key = linkrandom(32)
6015 try:
6016 key = key.decode('utf-8')
6017 except AttributeError:
6018 pass
6019
6020 link_key = f"{session_obj.name}{linkChar}{invite_type}{linkChar}{invitee_name}{linkChar}{creation_date}{linkChar}{key}"
6021
6022 try:
6023 invite_link = models.InviteLinks(
6024 link=link_key,
6025 host_user=current_user.name,
6026 host_project=project.name,
6027 type=(invite_type == 'active'),
6028 creation_date=datetime.datetime.now(),
6029 max_uses=max_uses,
6030 use_count=0,
6031 id_sessions=session_id,
6032 id_users=invitee.id if invite_type == 'active' else None
6033 )
6034 db.session.add(invite_link)
6035 db.session.commit()
6036
6037 full_link = f"https://{DEFAULT_URL}/join/{link_key}"
6038 flash(f"Invitation link created successfully: {full_link}")
6039
6040 except Exception as e:
6041 db.session.rollback()
6042 logging.error(f"Error creating invite link: {str(e)}")
6043 flash("Failed to create invitation link")
6044
6045 elif action == 'revoke_invite':
6046 invite_id = request.form.get('invite_id')
6047 invite_link = db.session.query(models.InviteLinks).filter_by(
6048 id=invite_id,
6049 host_project=project.name
6050 ).first()
6051
6052 if invite_link:
6053 invite_link.is_revoked = True
6054 db.session.commit()
6055 flash("Invitation link revoked successfully")
6056 else:
6057 flash("Invitation link not found")
6058
6059 return redirect(url_for('.project_invite', project_id=project_id))
6060
6061 return render_template('project_invite.html',
6062 title=f"Invite Users - {project.name}",
6063 project=project,
6064 sessions=sessions,
6065 invite_links=invite_links,
6066 **myrender())
6067
6068# Email Verification Routes
6069@app.route('/verify-email/<token>')
6070def verify_email(token):
6071 """Verify user email with JWT token"""
6072 try:
6073 # Verify the token
6074 data = verify_token(token, app.secret_key)
6075 if data is None:
6076 flash('Invalid or expired verification link.', 'error')
6077 return redirect('/register')
6078
6079 user_id = data.get('confirm_id')
6080 if not user_id:
6081 flash('Invalid verification link.', 'error')
6082 return redirect('/register')
6083
6084 # Find user and verify email
6085 user = db.session.query(models.Users).filter_by(id=user_id).first()
6086 if not user:
6087 flash('User not found.', 'error')
6088 return redirect('/register')
6089
6090 if user.is_verified:
6091 flash('User already verified. You can login.',"success")
6092 return redirect('/login')
6093
6094 # Mark user as verified
6095 user.is_verified = True
6096 user.dateverified = datetime.datetime.now()
6097 db.session.commit()
6098
6099 # Delete the sent email from IMAP (only if user has email)
6100 if user.mail:
6101 delete_sent_email("TiledViz - Email Verification", user.mail)
6102
6103 flash('Email verified successfully! You can now log in.', 'success')
6104 return redirect('/login')
6105
6106 except Exception as e:
6107 logging.error(f"Error verifying email: {e}")
6108 flash('An error occurred during verification. Please try again.', 'error')
6109 return redirect('/register')
6110
6111@app.route('/resend-verification', methods=['GET', 'POST'])
6112def resend_verification():
6113 """Resend verification email"""
6114 if request.method == 'POST':
6115 email = request.form.get('email')
6116 if not email:
6117 flash('Please enter your email address.', 'error')
6118 return render_template('resend_verification.html', **myrender())
6119
6120 # Find user
6121 user = db.session.query(models.Users).filter_by(mail=email).first()
6122 if not user:
6123 flash('No account found with this email address.', 'error')
6124 return render_template('resend_verification.html', **myrender())
6125
6126 if user.is_verified:
6127 flash('This email is already verified. You can log in.', 'info')
6128 return redirect('/login')
6129
6130 # Generate new token and send email
6131 token = generate_verification_token(user.id)
6132 email_sent = send_verification_email(
6133 user_email=user.mail,
6134 username=user.name,
6135 token=token
6136 )
6137
6138 if email_sent:
6139 flash('Verification email sent! Please check your inbox.', 'success')
6140 else:
6141 flash('Failed to send verification email. Please try again later.', 'error')
6142
6143 return redirect('/login')
6144
6145 return render_template('resend_verification.html', **myrender())
6146
pca_on_multiple_nodes(nodes_json_text)
Definition anatreada.py:297
pca_on_one_node(nodes_json_text)
Definition anatreada.py:77