From 23c2d711aef4a7370fe14442fdb6d093aa6d913e Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Mon, 4 Aug 2025 15:13:56 -0600 Subject: [PATCH 01/29] #327 modification on the way of store the date ad user who modify the row of the summary --- climmob/config/routes.py | 12 ++ climmob/processes/db/project.py | 11 ++ climmob/templates/403.jinja2 | 28 +++++ climmob/templates/dashboard/dashboard.jinja2 | 30 ++++- .../project/editData/editData.jinja2 | 19 ++- .../project/enumerators/enumerators.jinja2 | 51 ++++++--- .../templates/project/finishproject.jinja2 | 108 ++++++++++++++++++ .../project/metadata/metadata.jinja2 | 28 ++++- .../templates/project/modifyproject.jinja2 | 13 ++- climmob/templates/project/share/share.jinja2 | 21 ++-- .../project/assessment/assessmentform.jinja2 | 22 +++- .../snippets/project/project_form.jinja2 | 19 ++- climmob/utility/email.py | 1 + climmob/views/allowed_on_finish_project.py | 10 ++ climmob/views/basic_views.py | 4 + climmob/views/classes.py | 9 +- climmob/views/dashboard.py | 1 - climmob/views/enumerator.py | 19 ++- climmob/views/project.py | 44 ++++++- climmob/views/project_enumerators.py | 2 + 20 files changed, 401 insertions(+), 51 deletions(-) create mode 100644 climmob/templates/403.jinja2 create mode 100644 climmob/templates/project/finishproject.jinja2 create mode 100644 climmob/views/allowed_on_finish_project.py diff --git a/climmob/config/routes.py b/climmob/config/routes.py index cb06dd1d..c92af259 100644 --- a/climmob/config/routes.py +++ b/climmob/config/routes.py @@ -157,6 +157,7 @@ HomeView, HealthView, NotFoundView, + Forbidden, LoginView, RegisterView, LogoutView, @@ -212,6 +213,7 @@ CurationOfProjectsView, GetUnitOfAnalysisByLocationView, GetObjectivesByLocationAndUnitOfAnalysisView, + FinishProjectView ) from climmob.views.projectHelp.projectHelp import projectHelp_view from climmob.views.project_analysis import analysisDataView @@ -919,6 +921,14 @@ def loadRoutes(config): "project/closepregistry.jinja2", ) ) + routes.append( + addRoute( + "finishproject", + "/finishproject", + FinishProjectView, + "project/finishproject.jinja2", + ) + ) # Assessment routes.append( @@ -2185,6 +2195,8 @@ def loadRoutes(config): config.add_notfound_view(NotFoundView, renderer="404.jinja2") + config.add_forbidden_view(Forbidden, renderer="403.jinja2") + # Custom mapping can happen here AFTER the host maps for plugin in p.PluginImplementations(p.IRoutes): routes = plugin.after_mapping(config) diff --git a/climmob/processes/db/project.py b/climmob/processes/db/project.py index b86062d0..d834e901 100644 --- a/climmob/processes/db/project.py +++ b/climmob/processes/db/project.py @@ -62,6 +62,7 @@ "getProjectFullDetailsById", "getProjectsByUserThatRequireSetup", "update_project_status", + "update_project_finish", ] @@ -948,3 +949,13 @@ def update_project_status(project_id, status, request): return True except Exception as e: return False, str(e) + +def update_project_finish(request, project_id): + try: + request.dbsession.query(Project).filter( + Project.project_id == project_id + ).update({"project_status": 3}) + + return True, "" + except Exception as e: + return False, str(e) \ No newline at end of file diff --git a/climmob/templates/403.jinja2 b/climmob/templates/403.jinja2 new file mode 100644 index 00000000..b505dde5 --- /dev/null +++ b/climmob/templates/403.jinja2 @@ -0,0 +1,28 @@ + + + + + + + {{ _("ClimMob | 403 Error") }} + + {% block css %} + {% cssresource request,'coreresources','style' %} + {% endblock css %} + + + + +
+

403

+

{{ _("Forbidden") }}

+ +
+ {{ _("Sorry, but the acction is not allowed.") }}
+ {{ _('Actual project was mark like finished, is not allow to edit it.') }} + {{ _("You can go back to main page") }}:
{{ _("Home") }} +
+
+ + + diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index d70caf03..f6e6e424 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -563,7 +563,7 @@ {% if progress.regtotal > 0 %} - {{ _("View and edit data") }} + {% if activeProject["project_status"] != 3 %} {{ _("View and edit data") }} {% else %} {{ _('view data') }} {% endif %} {{ _("Download data in .CSV format") }} {{ _("Download data in .XLSX format") }} @@ -701,7 +701,7 @@ {% block rhomis_btns_for_datacollection scoped%} {% if assessment.asstotal > 0 %} - {{ _("View and edit data") }} + {% if activeProject["project_status"] != 3 %} {{ _("View and edit data") }} {% else %} {{ _('view data') }} {% endif %} {{ _("Download data in .CSV format") }} {{ _("Download data in .XLSX format") }} @@ -751,7 +751,6 @@ {% endfor %} {% endif %} - {% if activeProject["project_template"] != 1 %} {% if total_ass_records > 5 or (activeProject.project_registration_and_analysis == 1 and progress.regtotal >=5)%} {% if activeProject["access_type"] not in [4] %} @@ -774,6 +773,29 @@ {% endif %} {% endif %} {% endif %} + {% if hasActiveProject and activeProject["project_status"] != 3 %} +
+
+
{{ _("Finish the project") }}
+
+
+

{{ _('If you are complete sure that you project is finish press the button.') }}

+

{{ _('This action will be block all the functions to add or modify the project.') }}

+
+ + +
+
+
+ {% endif %} + + + + + + {% block communityLink %} @@ -786,7 +808,6 @@ - {% block piesFieldAgents %} {% if progress.regtotal > 0 %} @@ -843,7 +864,6 @@ {% endif %} - {% for fieldAgentAssess in fieldagents["Assessments"] %} {% if fieldAgentAssess["Values"] %} diff --git a/climmob/templates/project/modifyproject.jinja2 b/climmob/templates/project/modifyproject.jinja2 index 43a1bb2e..e744f34a 100644 --- a/climmob/templates/project/modifyproject.jinja2 +++ b/climmob/templates/project/modifyproject.jinja2 @@ -38,6 +38,7 @@ {% block pagecontent %} {% if newproject == False %} + {% if error_summary %} {% for key, error in error_summary.items() %}
@@ -49,7 +50,15 @@
+ + {% block newprjform %} + + {% set finished=False %} + {% if activeProject["project_status"] == 3 %} + {% set finished=True %} + {% endif %} + {% set edit=True%} {% if activeProject.access_type != 4%} {% set permissionForChanges=True%} @@ -62,6 +71,7 @@ {% set showNote=True %} {% include 'snippets/project/project_form.jinja2' %} + {% if not finished %} {% if permissionForChanges %}
@@ -74,18 +84,19 @@
+



{% endif %} + {% endif %} {% endblock newprjform %}


- {% include 'snippets/languages/addMoreLanguages.jinja2' %} \ No newline at end of file diff --git a/climmob/templates/snippets/project/project_form.jinja2 b/climmob/templates/snippets/project/project_form.jinja2 index f23986a8..f7e054ef 100644 --- a/climmob/templates/snippets/project/project_form.jinja2 +++ b/climmob/templates/snippets/project/project_form.jinja2 @@ -1,3 +1,7 @@ +{% if finished %} +
+{% endif %} +
@@ -360,6 +364,8 @@
{% endif %} + + {% block project_section_extra %} {% endblock project_section_extra %} @@ -367,7 +373,9 @@
- +{% if finished %} + +{% endif %} diff --git a/climmob/utility/email.py b/climmob/utility/email.py index e1e5488e..9c116c55 100644 --- a/climmob/utility/email.py +++ b/climmob/utility/email.py @@ -13,4 +13,5 @@ def build_email_message(body, subject, target_name, target_email, mail_from): recipient = "{} <{}>".format(target_name.encode("utf-8"), target_email) msg["To"] = Header(recipient, "utf-8") msg["Date"] = utils.formatdate(time()) + return msg diff --git a/climmob/views/allowed_on_finish_project.py b/climmob/views/allowed_on_finish_project.py new file mode 100644 index 00000000..6355aa52 --- /dev/null +++ b/climmob/views/allowed_on_finish_project.py @@ -0,0 +1,10 @@ +def is_allowed_exception(request): + path = request.path.lower() + + # allowed exeptions when the useer marks the project as complete + if path.endswith("/project/new"): + return True + if path.endswith("/editprofile"): + return True + + return False \ No newline at end of file diff --git a/climmob/views/basic_views.py b/climmob/views/basic_views.py index 6a371c89..77198f14 100644 --- a/climmob/views/basic_views.py +++ b/climmob/views/basic_views.py @@ -113,6 +113,10 @@ def get(self): self.request.response.status = 404 return {} +class Forbidden(publicView): + def get(self): + self.request.response.status = 403 + return {} class StoreCookieView(publicView): def post(self): diff --git a/climmob/views/classes.py b/climmob/views/classes.py index a03bcce8..1c1fb7ca 100644 --- a/climmob/views/classes.py +++ b/climmob/views/classes.py @@ -12,7 +12,7 @@ HTTPFound, HTTPMethodNotAllowed, HTTPBadRequest, - HTTPClientError, + HTTPClientError, HTTPForbidden, ) from pyramid.httpexceptions import HTTPNotFound from pyramid.response import Response @@ -20,6 +20,7 @@ import climmob.plugins as p from climmob.config.auth import getUserData, getUserByApiKey +from climmob.views.allowed_on_finish_project import is_allowed_exception from climmob.views.context.ApiContext import ApiContext from climmob.views.context.PrivateContext import PrivateContext from climmob.views.validators import Field, FieldValidator @@ -364,6 +365,7 @@ def __init__(self, request): "showHelp": False, "showRememberAfterCreateProject": False, "surveyMustBeDisplayed": None, + "project_status": None, } self.viewResult = {} @@ -391,6 +393,7 @@ def __call__(self): if activeProjectData: self.classResult["hasActiveProject"] = True self.classResult["activeProject"] = activeProjectData["project_id"] + self.classResult["project_status"] = activeProjectData["project_status"] else: self.classResult["hasActiveProject"] = False @@ -436,6 +439,10 @@ def __call__(self): self.request.session.pop_flash() log.error("SECURITY-CSRF error at {} ".format(self.request.url)) raise HTTPNotFound() + if activeProjectData["project_status"] == 3: + if not is_allowed_exception(self.request): + self.request.method = "GET" + raise HTTPForbidden() else: if self.checkCrossPost: if self.request.referer != self.request.url: diff --git a/climmob/views/dashboard.py b/climmob/views/dashboard.py index 8b954b9a..cac71f10 100644 --- a/climmob/views/dashboard.py +++ b/climmob/views/dashboard.py @@ -77,7 +77,6 @@ def processView(self): total_ass_records = total_ass_records + assessment["asstotal"] else: all_ass_closed = False - context = { "activeUser": self.user, "activeProject": activeProjectData, diff --git a/climmob/views/enumerator.py b/climmob/views/enumerator.py index d2f45289..ef86ec39 100644 --- a/climmob/views/enumerator.py +++ b/climmob/views/enumerator.py @@ -24,16 +24,15 @@ class getEnumeratorDetails_view(privateView): - def processView(self): - if self.request.method == "GET": - userOwner = self.request.matchdict["user"] - enumId = self.request.matchdict["enumid"] - enumerator = getEnumeratorData(userOwner, enumId, self.request) - self.returnRawViewResult = True - for plugin in p.PluginImplementations(p.IEnumerator): - enumerator = plugin.before_returning_context(self.request, enumerator) - return enumerator - raise HTTPNotFound + def get(self): + userOwner = self.request.matchdict["user"] + enumId = self.request.matchdict["enumid"] + enumerator = getEnumeratorData(userOwner, enumId, self.request) + self.returnRawViewResult = True + for plugin in p.PluginImplementations(p.IEnumerator): + enumerator = plugin.before_returning_context(self.request, enumerator) + return enumerator + class enumerators_view(privateView): diff --git a/climmob/views/project.py b/climmob/views/project.py index 668c60d0..28cd3570 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -9,7 +9,6 @@ addProject, getProjectData, modifyProject, - projectExists, deleteProject, changeTheStateOfCreateComb, getCountryList, @@ -52,6 +51,7 @@ get_location_unit_of_analysis_objectives_by_combination, delete_all_project_location_unit_objective, get_all_affiliations, + update_project_finish ) from climmob.views.classes import privateView from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator @@ -941,3 +941,45 @@ def processView(self): return objectives return {} + + +class FinishProjectView(privateView): + validator = (ProjectExistsValidator,) + + def get(self): + user = self.request.GET.get('user') + project = self.request.GET.get('project') + project_info= getActiveProject(user, self.request) + + return { + 'user': user, + 'project': project, + 'project_info': project_info, + } + + def post(self): + + user = self.request.POST.get('user') + project = self.request.POST.get('project') + + success, error_update = update_project_finish(self.request, project) + project_info = getActiveProject(user, self.request) + if success: + #todo check the persson to send the email + + + self._redirect = HTTPFound(location=self.request.route_url('finishproject')) + return { + 'success': True, + 'user': user, + 'project': project, + 'project_info': project_info, + } + else: + self._redirect = HTTPFound(location=self.request.route_url('finishproject')) + return{ + 'error': error_update, + 'user': user, + 'project': project, + 'project_info': project_info, + } diff --git a/climmob/views/project_enumerators.py b/climmob/views/project_enumerators.py index 0067ddd0..974d0028 100644 --- a/climmob/views/project_enumerators.py +++ b/climmob/views/project_enumerators.py @@ -32,6 +32,7 @@ def processView(self): activeProjectUser, activeProjectCod, self.request ) activeProject = getActiveProject(self.user.login, self.request) + print(self.classResult) if activeProject["project_template"] == 1: @@ -42,6 +43,7 @@ def processView(self): _query={ "user": activeProjectUser, "project": activeProjectCod, + "project_status": self.classResult["project_status"], }, ) ) From ab61819f5fb7ec0b71891672ce486049cbab9434 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Tue, 5 Aug 2025 12:31:58 -0600 Subject: [PATCH 02/29] #327 change the trigger to the class and now we use validator ProjectExistsValidator --- climmob/views/Api/projectCreation.py | 261 ++++++----- climmob/views/Api/questions.py | 1 + climmob/views/Share/projectShare.py | 1 + climmob/views/allowed_on_finish_project.py | 10 - climmob/views/classes.py | 5 - climmob/views/editData.py | 2 + climmob/views/project_enumerators.py | 148 ++++--- climmob/views/project_metadata.py | 3 + climmob/views/project_technologies.py | 418 ++++++++++++------ climmob/views/registry.py | 6 + .../validators/ProjectExistsValidator.py | 12 +- 11 files changed, 506 insertions(+), 361 deletions(-) delete mode 100644 climmob/views/allowed_on_finish_project.py diff --git a/climmob/views/Api/projectCreation.py b/climmob/views/Api/projectCreation.py index b863f97e..7f167d14 100644 --- a/climmob/views/Api/projectCreation.py +++ b/climmob/views/Api/projectCreation.py @@ -41,6 +41,7 @@ ) from climmob.views.classes import apiView from climmob.views.project import function_create_clone +from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator class ReadListOfTemplatesView(apiView): @@ -1364,87 +1365,79 @@ def myconverter(o): class AddCollaboratorView(apiView): - def processView(self): + validators = (ProjectExistsValidator,) + + def post(self): + + obligatory = [ + "project_cod", + "user_owner", + "user_collaborator", + "access_type", + ] + dataworking = json.loads(self.body) + + if sorted(obligatory) == sorted(dataworking.keys()): + + if not projectExists( + self.user.login, + dataworking["user_owner"], + dataworking["project_cod"], + self.request, + ): + response = Response( + status=401, body=self._("This project does not exist.") + ) + return response - if self.request.method == "POST": + activeProjectId = getTheProjectIdForOwner( + dataworking["user_owner"], dataworking["project_cod"], self.request + ) + accessType = getAccessTypeForProject( + self.user.login, activeProjectId, self.request + ) - obligatory = [ - "project_cod", - "user_owner", - "user_collaborator", - "access_type", - ] - dataworking = json.loads(self.body) + if accessType in [4]: + response = Response( + status=401, + body=self._( + "The access assigned for this project does not allow you to add collaborators to the project." + ), + ) + return response - if sorted(obligatory) == sorted(dataworking.keys()): + if getUserInfo(self.request, dataworking["user_collaborator"]): - if not projectExists( - self.user.login, - dataworking["user_owner"], - dataworking["project_cod"], + if not theUserBelongsToTheProject( + dataworking["user_collaborator"], + activeProjectId, self.request, ): - response = Response( - status=401, body=self._("This project does not exist.") - ) - return response - - activeProjectId = getTheProjectIdForOwner( - dataworking["user_owner"], dataworking["project_cod"], self.request - ) - accessType = getAccessTypeForProject( - self.user.login, activeProjectId, self.request - ) - - if accessType in [4]: - response = Response( - status=401, - body=self._( - "The access assigned for this project does not allow you to add collaborators to the project." - ), - ) - return response - - if getUserInfo(self.request, dataworking["user_collaborator"]): + if dataworking["access_type"] in [2, 3, 4, "2", "3", "4"]: + + dataworking["access_type"] = int(dataworking["access_type"]) + dataworking["project_id"] = activeProjectId + dataworking["user_name"] = dataworking["user_collaborator"] + dataworking["project_dashboard"] = 0 + added, message = add_project_collaborator( + self.request, dataworking + ) - if not theUserBelongsToTheProject( - dataworking["user_collaborator"], - activeProjectId, - self.request, - ): - if dataworking["access_type"] in [2, 3, 4, "2", "3", "4"]: - - dataworking["access_type"] = int(dataworking["access_type"]) - dataworking["project_id"] = activeProjectId - dataworking["user_name"] = dataworking["user_collaborator"] - dataworking["project_dashboard"] = 0 - added, message = add_project_collaborator( - self.request, dataworking + if added: + response = Response( + status=200, + body=self._("Collaborator added successfully."), ) + return response - if added: - response = Response( - status=200, - body=self._("Collaborator added successfully."), - ) - return response - - else: - response = Response(status=401, body=message) - return response else: - response = Response( - status=401, - body=self._( - "The types of access for collaborators are as follows: 2=Admin, 3=Editor, 4=Member." - ), - ) + response = Response(status=401, body=message) return response else: response = Response( status=401, body=self._( - "The collaborator you want to add already belongs to the project." + "The types of access for collaborators are as follows: 2=Admin, 3=Editor, 4=Member." ), ) return response @@ -1452,99 +1445,97 @@ def processView(self): response = Response( status=401, body=self._( - "The user you want to add as a collaborator does not exist." + "The collaborator you want to add already belongs to the project." ), ) return response else: - response = Response(status=401, body=self._("Error in the JSON.")) + response = Response( + status=401, + body=self._( + "The user you want to add as a collaborator does not exist." + ), + ) return response else: - response = Response(status=401, body=self._("Only accepts POST method.")) + response = Response(status=401, body=self._("Error in the JSON.")) return response -class DeleteCollaboratorView(apiView): - def processView(self): - if self.request.method == "POST": - obligatory = ["project_cod", "user_owner", "user_collaborator"] - dataworking = json.loads(self.body) +class DeleteCollaboratorView(apiView): + validators = (ProjectExistsValidator,) - if sorted(obligatory) == sorted(dataworking.keys()): + def post(self): + obligatory = ["project_cod", "user_owner", "user_collaborator"] + dataworking = json.loads(self.body) - if not projectExists( - self.user.login, - dataworking["user_owner"], - dataworking["project_cod"], - self.request, - ): - response = Response( - status=401, body=self._("This project does not exist.") - ) - return response + if sorted(obligatory) == sorted(dataworking.keys()): - activeProjectId = getTheProjectIdForOwner( - dataworking["user_owner"], dataworking["project_cod"], self.request - ) - accessType = getAccessTypeForProject( - self.user.login, activeProjectId, self.request + if not projectExists( + self.user.login, + dataworking["user_owner"], + dataworking["project_cod"], + self.request, + ): + response = Response( + status=401, body=self._("This project does not exist.") ) + return response - if accessType in [4]: - response = Response( - status=401, - body=self._( - "The access assigned for this project does not allow you to delete collaborators from the project." - ), - ) - return response + activeProjectId = getTheProjectIdForOwner( + dataworking["user_owner"], dataworking["project_cod"], self.request + ) + accessType = getAccessTypeForProject( + self.user.login, activeProjectId, self.request + ) + + if accessType in [4]: + response = Response( + status=401, + body=self._( + "The access assigned for this project does not allow you to delete collaborators from the project." + ), + ) + return response - if getUserInfo(self.request, dataworking["user_collaborator"]): + if getUserInfo(self.request, dataworking["user_collaborator"]): - if theUserBelongsToTheProject( - dataworking["user_collaborator"], - activeProjectId, - self.request, + if theUserBelongsToTheProject( + dataworking["user_collaborator"], + activeProjectId, + self.request, + ): + if ( + dataworking["user_owner"] + != dataworking["user_collaborator"] ): - if ( - dataworking["user_owner"] - != dataworking["user_collaborator"] - ): - remove, message = remove_collaborator( - self.request, - activeProjectId, - dataworking["user_collaborator"], - self, - ) - - if remove: - response = Response( - status=200, - body=self._( - "The collaborator has been successfully removed." - ), - ) - return response + remove, message = remove_collaborator( + self.request, + activeProjectId, + dataworking["user_collaborator"], + self, + ) - else: - response = Response(status=401, body=message) - return response - else: + if remove: response = Response( - status=401, + status=200, body=self._( - "The user who owns the project cannot be deleted." + "The collaborator has been successfully removed." ), ) return response + + else: + response = Response(status=401, body=message) + return response else: response = Response( status=401, body=self._( - "You are trying to delete a collaborator that does not belong to this project." + "The user who owns the project cannot be deleted." ), ) return response @@ -1552,13 +1543,19 @@ def processView(self): response = Response( status=401, body=self._( - "The user you want to delete as a collaborator does not exist." + "You are trying to delete a collaborator that does not belong to this project." ), ) return response else: - response = Response(status=401, body=self._("Error in the JSON.")) + response = Response( + status=401, + body=self._( + "The user you want to delete as a collaborator does not exist." + ), + ) return response else: - response = Response(status=401, body=self._("Only accepts POST method.")) + response = Response(status=401, body=self._("Error in the JSON.")) return response + diff --git a/climmob/views/Api/questions.py b/climmob/views/Api/questions.py index b9b292e0..5f88b138 100644 --- a/climmob/views/Api/questions.py +++ b/climmob/views/Api/questions.py @@ -39,6 +39,7 @@ class CreateQuestionView(apiView): validators = (QuestionMinMaxValidator,) + def post(self): possibles = [ diff --git a/climmob/views/Share/projectShare.py b/climmob/views/Share/projectShare.py index caf84ee6..90385006 100644 --- a/climmob/views/Share/projectShare.py +++ b/climmob/views/Share/projectShare.py @@ -230,6 +230,7 @@ def processView(self): class removeprojectShare_view(privateView): + validators = (ProjectExistsValidator,) def processView(self): collaborator = self.request.matchdict["collaborator"] diff --git a/climmob/views/allowed_on_finish_project.py b/climmob/views/allowed_on_finish_project.py deleted file mode 100644 index 6355aa52..00000000 --- a/climmob/views/allowed_on_finish_project.py +++ /dev/null @@ -1,10 +0,0 @@ -def is_allowed_exception(request): - path = request.path.lower() - - # allowed exeptions when the useer marks the project as complete - if path.endswith("/project/new"): - return True - if path.endswith("/editprofile"): - return True - - return False \ No newline at end of file diff --git a/climmob/views/classes.py b/climmob/views/classes.py index 1c1fb7ca..4b3803ea 100644 --- a/climmob/views/classes.py +++ b/climmob/views/classes.py @@ -20,7 +20,6 @@ import climmob.plugins as p from climmob.config.auth import getUserData, getUserByApiKey -from climmob.views.allowed_on_finish_project import is_allowed_exception from climmob.views.context.ApiContext import ApiContext from climmob.views.context.PrivateContext import PrivateContext from climmob.views.validators import Field, FieldValidator @@ -439,10 +438,6 @@ def __call__(self): self.request.session.pop_flash() log.error("SECURITY-CSRF error at {} ".format(self.request.url)) raise HTTPNotFound() - if activeProjectData["project_status"] == 3: - if not is_allowed_exception(self.request): - self.request.method = "GET" - raise HTTPForbidden() else: if self.checkCrossPost: if self.request.referer != self.request.url: diff --git a/climmob/views/editData.py b/climmob/views/editData.py index d990ccea..6872976f 100755 --- a/climmob/views/editData.py +++ b/climmob/views/editData.py @@ -15,6 +15,7 @@ from climmob.products.analysisdata.analysisdata import create_datacsv from climmob.products.dataxlsx.dataxlsx import create_XLSXToDownload from climmob.products.errorLogDocument.errorLogDocument import create_error_log_document +from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator from climmob.views.classes import privateView from climmob.views.editDataDB import ( getNamesEditByColums, @@ -188,6 +189,7 @@ def processView(self): class editDataView(privateView): + validators = (ProjectExistsValidator,) def processView(self): activeProjectUser = self.request.matchdict["user"] diff --git a/climmob/views/project_enumerators.py b/climmob/views/project_enumerators.py index 974d0028..93a77bcf 100644 --- a/climmob/views/project_enumerators.py +++ b/climmob/views/project_enumerators.py @@ -15,50 +15,65 @@ from climmob.views.classes import privateView import climmob.plugins as p +from climmob.views.validators import TextField, IntegerField, BinaryField +from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator + class projectEnumerators_view(privateView): - def processView(self): + validators = (ProjectExistsValidator,) + + def get(self): activeProjectUser = self.request.matchdict["user"] activeProjectCod = self.request.matchdict["project"] error_summary = {} - if not projectExists( - self.user.login, activeProjectUser, activeProjectCod, self.request - ): - raise HTTPNotFound() - else: - activeProjectId = getTheProjectIdForOwner( - activeProjectUser, activeProjectCod, self.request - ) - activeProject = getActiveProject(self.user.login, self.request) - print(self.classResult) - - if activeProject["project_template"] == 1: - - self.returnRawViewResult = True - return HTTPFound( - location=self.request.route_url( - "dashboard", - _query={ - "user": activeProjectUser, - "project": activeProjectCod, - "project_status": self.classResult["project_status"], - }, - ) + activeProjectId = getTheProjectIdForOwner( + activeProjectUser, activeProjectCod, self.request + ) + activeProject = getActiveProject(self.user.login, self.request) + if activeProject["project_template"] == 1: + self.returnRawViewResult = True + + return HTTPFound( + location=self.request.route_url( + "dashboard", + _query={ + "user": activeProjectUser, + "project": activeProjectCod, + "project_status": self.classResult["project_status"], + }, ) + ) - if self.request.method == "POST": - error_summary = addProjectEnumerators_view.processView(self) - return { - "activeUser": self.user, - "activeProject": activeProject, - "enumeratorsInProject": getProjectEnumerators( - activeProjectId, self.request - ), - "enumerators": getUsableEnumerators(activeProjectId, self.request), - "error_summary": error_summary, - } + return { + "activeUser": self.user, + "activeProject": activeProject, + "enumeratorsInProject": getProjectEnumerators( + activeProjectId, self.request + ), + "enumerators": getUsableEnumerators(activeProjectId, self.request), + "error_summary": error_summary, + } + def post(self): + activeProjectUser = self.request.matchdict["user"] + activeProjectCod = self.request.matchdict["project"] + error_summary = {} + activeProjectId = getTheProjectIdForOwner( + activeProjectUser, activeProjectCod, self.request + ) + activeProject = getActiveProject(self.user.login, self.request) + error_summary = addProjectEnumerators_view.processView(self) + + return { + "activeUser": self.user, + "activeProject": activeProject, + "enumeratorsInProject": getProjectEnumerators( + activeProjectId, self.request + ), + "enumerators": getUsableEnumerators(activeProjectId, self.request), + "error_summary": error_summary, + } class addProjectEnumerators_view(privateView): @@ -143,46 +158,37 @@ def processView(self): class removeProjectEnumerators_view(privateView): - def processView(self): + validators = (ProjectExistsValidator,) + def post(self): enumeratorid = self.request.matchdict["enumeratorid"] activeProjectUser = self.request.matchdict["user"] activeProjectCod = self.request.matchdict["project"] - if not projectExists( - self.user.login, activeProjectUser, activeProjectCod, self.request - ): - raise HTTPNotFound() + activeProjectId = getTheProjectIdForOwner( + activeProjectUser, activeProjectCod, self.request + ) + deleted, message = removeEnumeratorFromProject( + activeProjectId, enumeratorid, self.request + ) + if not deleted: + self.returnRawViewResult = True + return {"status": 400, "error": message} else: - - activeProjectId = getTheProjectIdForOwner( - activeProjectUser, activeProjectCod, self.request + stopTasksByProcess( + self.request, + activeProjectId, + processName="create_fieldagents", ) - - if self.request.method == "POST": - deleted, message = removeEnumeratorFromProject( - activeProjectId, enumeratorid, self.request - ) - if not deleted: - self.returnRawViewResult = True - return {"status": 400, "error": message} - else: - stopTasksByProcess( - self.request, - activeProjectId, - processName="create_fieldagents", - ) - locale = self.request.locale_name - create_fieldagents_report( - locale, - self.request, - activeProjectUser, - activeProjectCod, - activeProjectId, - getProjectEnumerators(activeProjectId, self.request), - getActiveProject(self.user.login, self.request), - ) - self.returnRawViewResult = True - return {"status": 200} - else: - return {} + locale = self.request.locale_name + create_fieldagents_report( + locale, + self.request, + activeProjectUser, + activeProjectCod, + activeProjectId, + getProjectEnumerators(activeProjectId, self.request), + getActiveProject(self.user.login, self.request), + ) + self.returnRawViewResult = True + return {"status": 200} diff --git a/climmob/views/project_metadata.py b/climmob/views/project_metadata.py index ab7ae341..2177b07f 100644 --- a/climmob/views/project_metadata.py +++ b/climmob/views/project_metadata.py @@ -15,6 +15,7 @@ get_all_affiliations, languageByLanguageCode, ) +from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator from climmob.views.classes import privateView from jinja2 import Environment, FileSystemLoader import json @@ -22,6 +23,8 @@ class ProjectMetadataFormView(privateView): + validators = (ProjectExistsValidator,) + def processView(self): activeProjectUser = self.request.matchdict["user"] diff --git a/climmob/views/project_technologies.py b/climmob/views/project_technologies.py index abb96eb7..03ba040f 100644 --- a/climmob/views/project_technologies.py +++ b/climmob/views/project_technologies.py @@ -28,165 +28,299 @@ class projectTecnologies_view(privateView): - def processView(self): + validators = (ProjectExistsValidator,) + def get(self): activeProjectUser = self.request.matchdict["user"] activeProjectCod = self.request.matchdict["project"] - alias = {} - tech_id = "" - dataworking = {} - error_summary = {} - error_summary2 = {} - dataworking["alias_name"] = "" - techSee = {} - listOfCombinations = [] - - if not projectExists( - self.user.login, activeProjectUser, activeProjectCod, self.request - ): - raise HTTPNotFound() - else: - activeProjectId = getTheProjectIdForOwner( - activeProjectUser, activeProjectCod, self.request - ) - - prjData = getProjectData(activeProjectId, self.request) - - if prjData["project_regstatus"] > 0: - listOfCombinations = getCombinationsData(activeProjectId, self.request) - - if prjData["project_template"] == 1: - self.returnRawViewResult = True - return HTTPFound( - location=self.request.route_url( - "dashboard", - _query={ - "user": activeProjectUser, - "project": activeProjectCod, - }, - ) - ) - - # Only create the packages if its needed - if prjData["project_createpkgs"] == 2: - self.returnRawViewResult = True - - return HTTPFound(location=self.request.route_url("dashboard")) - - if self.request.method == "POST": - if "btn_save_technologies" in self.request.POST: - postdata = self.getPostDict() + activeProjectId = getTheProjectIdForOwner( + activeProjectUser, activeProjectCod, self.request + ) - if postdata["txt_technologies_included"] != "": + prjData = getProjectData(activeProjectId, self.request) - part = postdata["txt_technologies_included"][:-1].split(",") + if prjData["project_template"] == 1 or prjData["project_createpkgs"] == 2: + self.returnRawViewResult = True + return HTTPFound(location=self.request.route_url( + "dashboard", + _query={"user": activeProjectUser, "project": activeProjectCod} + )) - for element in part: - attr = element.split("_") - if attr[2] == "new": - addTechnologyProject( - activeProjectId, - attr[1], - self.request, - ) - if postdata["txt_technologies_excluded"] != "": + listOfCombinations = [] + if prjData["project_regstatus"] > 0: + listOfCombinations = getCombinationsData(activeProjectId, self.request) - part = postdata["txt_technologies_excluded"][:-1].split(",") + technologiesInProject = searchTechnologiesInProject(activeProjectId, self.request) - for element in part: - attr = element.split("_") - if attr[2] == "exists": - deleteTechnologyProject( - activeProjectId, - attr[1], - self.request, - ) + totalOfCombinations = 1 + for tech in technologiesInProject: + totalOfCombinations *= tech["quantity"] - if "btn_show_technology_alias" in self.request.POST: - postdata = self.getPostDict() - tech_id = postdata["tech_id"] - self.request.matchdict["tech_id"] = postdata["tech_id"] - alias = prjTechAliases_view.processView(self) - techSee = getTechnology(postdata, self.request) - - if "btn_show_technology_alias_in_library" in self.request.POST: - postdata = self.getPostDict() - tech_id = postdata["tech_id"] - alias = { - "AliasTechnology": AliasSearchTechnology( - tech_id, activeProjectId, self.request - ) - } - techSee = getTechnology(postdata, self.request) + error_summary2 = {} + if totalOfCombinations > 50: + error_summary2["totalOfCombinations"] = self._( + "ClimMob has limited the number of possible combinations to 50, at the moment you are exceeding this number so you must remove technology options to be able to create the packages later." + ) - if "btn_save_technologies_alias" in self.request.POST: - postdata = self.getPostDict() - tech_id = postdata["tech_id"] - dataworking["project_id"] = activeProjectId - dataworking["tech_id"] = tech_id - dataworking["user_name"] = self.user.login - if not isTechnologyAssigned(dataworking, self.request): - added, message = addTechnologyProject( - activeProjectId, - dataworking["tech_id"], - self.request, - ) - self.request.matchdict["tech_id"] = postdata["tech_id"] - alias = prjTechAliases_view.processView(self) - techSee = getTechnology(postdata, self.request) + return { + "activeUser": self.user, + "activeProject": getActiveProject(self.user.login, self.request), + "tech_id": "", + "TechnologiesUser": searchTechnologies(activeProjectId, self.request), + "TechnologiesInProject": technologiesInProject, + "project_numcom": numberOfCombinationsForTheProject(activeProjectId, self.request), + "alias": {}, + "dataworking": {"alias_name": ""}, + "error_summary": {}, + "techSee": {}, + "error_summary2": error_summary2, + "totalOfCombinations": totalOfCombinations, + "combinations": listOfCombinations, + } - if "btn_add_alias" in self.request.POST: - postdata = self.getPostDict() - tech_id = postdata["tech_id"] - dataworking["project_id"] = activeProjectId - dataworking["tech_id"] = tech_id - dataworking["user_name"] = self.user.login - if not isTechnologyAssigned(dataworking, self.request): - added, message = addTechnologyProject( - activeProjectId, - dataworking["tech_id"], - self.request, - ) + def post(self): + activeProjectUser = self.request.matchdict["user"] + activeProjectCod = self.request.matchdict["project"] + activeProjectId = getTheProjectIdForOwner( + activeProjectUser, activeProjectCod, self.request + ) - self.request.matchdict["tech_id"] = postdata["tech_id"] - result = prjTechAliasAdd_view.processView(self) - dataworking = result["dataworking"] - error_summary = result["error_summary"] - if result["redirect"]: - dataworking["alias_name"] = "" - alias = prjTechAliases_view.processView(self) - techSee = getTechnology(postdata, self.request) - - technologiesInProject = searchTechnologiesInProject( - activeProjectId, self.request - ) - totalOfCombinations = 1 - for tech in technologiesInProject: - totalOfCombinations = totalOfCombinations * tech["quantity"] + postdata = self.getPostDict() + alias = {} + tech_id = "" + dataworking = {"alias_name": ""} + error_summary = {} + error_summary2 = {} + techSee = {} - if totalOfCombinations > 50: - error_summary2["totalOfCombinations"] = self._( - "ClimMob has limited the number of possible combinations to 50, at the moment you are exceeding this number so you must remove technology options to be able to create the packages later." + if "btn_save_technologies" in postdata: + if postdata["txt_technologies_included"]: + for element in postdata["txt_technologies_included"][:-1].split(","): + attr = element.split("_") + if attr[2] == "new": + addTechnologyProject(activeProjectId, attr[1], self.request) + + if postdata["txt_technologies_excluded"]: + for element in postdata["txt_technologies_excluded"][:-1].split(","): + attr = element.split("_") + if attr[2] == "exists": + deleteTechnologyProject(activeProjectId, attr[1], self.request) + + elif "btn_show_technology_alias" in postdata: + tech_id = postdata["tech_id"] + self.request.matchdict["tech_id"] = tech_id + alias = prjTechAliases_view.processView(self) + techSee = getTechnology(postdata, self.request) + + elif "btn_show_technology_alias_in_library" in postdata: + tech_id = postdata["tech_id"] + alias = { + "AliasTechnology": AliasSearchTechnology( + tech_id, activeProjectId, self.request ) + } + techSee = getTechnology(postdata, self.request) - return { - "activeUser": self.user, - "activeProject": getActiveProject(self.user.login, self.request), + elif "btn_save_technologies_alias" in postdata: + tech_id = postdata["tech_id"] + dataworking.update({ + "project_id": activeProjectId, "tech_id": tech_id, - "TechnologiesUser": searchTechnologies(activeProjectId, self.request), - "TechnologiesInProject": technologiesInProject, - "project_numcom": numberOfCombinationsForTheProject( - activeProjectId, self.request - ), - "alias": alias, - "dataworking": dataworking, - "error_summary": error_summary, - "techSee": techSee, - "error_summary2": error_summary2, - "totalOfCombinations": totalOfCombinations, - "combinations": listOfCombinations, - } + "user_name": self.user.login, + }) + if not isTechnologyAssigned(dataworking, self.request): + addTechnologyProject(activeProjectId, tech_id, self.request) + + self.request.matchdict["tech_id"] = tech_id + alias = prjTechAliases_view.processView(self) + techSee = getTechnology(postdata, self.request) + + elif "btn_add_alias" in postdata: + tech_id = postdata["tech_id"] + dataworking.update({ + "project_id": activeProjectId, + "tech_id": tech_id, + "user_name": self.user.login, + }) + if not isTechnologyAssigned(dataworking, self.request): + addTechnologyProject(activeProjectId, tech_id, self.request) + + self.request.matchdict["tech_id"] = tech_id + result = prjTechAliasAdd_view.processView(self) + dataworking = result["dataworking"] + error_summary = result["error_summary"] + if result["redirect"]: + dataworking["alias_name"] = "" + alias = prjTechAliases_view.processView(self) + techSee = getTechnology(postdata, self.request) + + + response_data = self.get() + response_data.update({ + "tech_id": tech_id, + "alias": alias, + "dataworking": dataworking, + "error_summary": error_summary, + "techSee": techSee, + }) + return response_data + + # def processView(self): + # + # activeProjectUser = self.request.matchdict["user"] + # activeProjectCod = self.request.matchdict["project"] + # + # alias = {} + # tech_id = "" + # dataworking = {} + # error_summary = {} + # error_summary2 = {} + # dataworking["alias_name"] = "" + # techSee = {} + # listOfCombinations = [] + # + # + # activeProjectId = getTheProjectIdForOwner( + # activeProjectUser, activeProjectCod, self.request + # ) + # + # prjData = getProjectData(activeProjectId, self.request) + # + # if prjData["project_regstatus"] > 0: + # listOfCombinations = getCombinationsData(activeProjectId, self.request) + # + # if prjData["project_template"] == 1: + # self.returnRawViewResult = True + # return HTTPFound( + # location=self.request.route_url( + # "dashboard", + # _query={ + # "user": activeProjectUser, + # "project": activeProjectCod, + # }, + # ) + # ) + # + # # Only create the packages if its needed + # if prjData["project_createpkgs"] == 2: + # self.returnRawViewResult = True + # + # return HTTPFound(location=self.request.route_url("dashboard")) + # + # if self.request.method == "POST": + # if "btn_save_technologies" in self.request.POST: + # postdata = self.getPostDict() + # + # if postdata["txt_technologies_included"] != "": + # + # part = postdata["txt_technologies_included"][:-1].split(",") + # + # for element in part: + # attr = element.split("_") + # if attr[2] == "new": + # addTechnologyProject( + # activeProjectId, + # attr[1], + # self.request, + # ) + # if postdata["txt_technologies_excluded"] != "": + # + # part = postdata["txt_technologies_excluded"][:-1].split(",") + # + # for element in part: + # attr = element.split("_") + # if attr[2] == "exists": + # deleteTechnologyProject( + # activeProjectId, + # attr[1], + # self.request, + # ) + # + # if "btn_show_technology_alias" in self.request.POST: + # postdata = self.getPostDict() + # tech_id = postdata["tech_id"] + # self.request.matchdict["tech_id"] = postdata["tech_id"] + # alias = prjTechAliases_view.processView(self) + # techSee = getTechnology(postdata, self.request) + # + # if "btn_show_technology_alias_in_library" in self.request.POST: + # postdata = self.getPostDict() + # tech_id = postdata["tech_id"] + # alias = { + # "AliasTechnology": AliasSearchTechnology( + # tech_id, activeProjectId, self.request + # ) + # } + # techSee = getTechnology(postdata, self.request) + # + # if "btn_save_technologies_alias" in self.request.POST: + # postdata = self.getPostDict() + # tech_id = postdata["tech_id"] + # dataworking["project_id"] = activeProjectId + # dataworking["tech_id"] = tech_id + # dataworking["user_name"] = self.user.login + # if not isTechnologyAssigned(dataworking, self.request): + # added, message = addTechnologyProject( + # activeProjectId, + # dataworking["tech_id"], + # self.request, + # ) + # self.request.matchdict["tech_id"] = postdata["tech_id"] + # alias = prjTechAliases_view.processView(self) + # techSee = getTechnology(postdata, self.request) + # + # if "btn_add_alias" in self.request.POST: + # postdata = self.getPostDict() + # tech_id = postdata["tech_id"] + # dataworking["project_id"] = activeProjectId + # dataworking["tech_id"] = tech_id + # dataworking["user_name"] = self.user.login + # if not isTechnologyAssigned(dataworking, self.request): + # added, message = addTechnologyProject( + # activeProjectId, + # dataworking["tech_id"], + # self.request, + # ) + # + # self.request.matchdict["tech_id"] = postdata["tech_id"] + # result = prjTechAliasAdd_view.processView(self) + # dataworking = result["dataworking"] + # error_summary = result["error_summary"] + # if result["redirect"]: + # dataworking["alias_name"] = "" + # alias = prjTechAliases_view.processView(self) + # techSee = getTechnology(postdata, self.request) + # + # technologiesInProject = searchTechnologiesInProject( + # activeProjectId, self.request + # ) + # totalOfCombinations = 1 + # for tech in technologiesInProject: + # totalOfCombinations = totalOfCombinations * tech["quantity"] + # + # if totalOfCombinations > 50: + # error_summary2["totalOfCombinations"] = self._( + # "ClimMob has limited the number of possible combinations to 50, at the moment you are exceeding this number so you must remove technology options to be able to create the packages later." + # ) + # + # return { + # "activeUser": self.user, + # "activeProject": getActiveProject(self.user.login, self.request), + # "tech_id": tech_id, + # "TechnologiesUser": searchTechnologies(activeProjectId, self.request), + # "TechnologiesInProject": technologiesInProject, + # "project_numcom": numberOfCombinationsForTheProject( + # activeProjectId, self.request + # ), + # "alias": alias, + # "dataworking": dataworking, + # "error_summary": error_summary, + # "techSee": techSee, + # "error_summary2": error_summary2, + # "totalOfCombinations": totalOfCombinations, + # "combinations": listOfCombinations, + # } class prjTechAliases_view(privateView): diff --git a/climmob/views/registry.py b/climmob/views/registry.py index 4e710471..2c8c1a2e 100644 --- a/climmob/views/registry.py +++ b/climmob/views/registry.py @@ -303,6 +303,8 @@ def processView(self): class RegistryFormCreationView(privateView): + validators = (ProjectExistsValidator,) + def processView(self): activeProjectUser = self.request.matchdict["user"] activeProjectCod = self.request.matchdict["project"] @@ -379,6 +381,7 @@ def processView(self): return "" + def getDataFormPreview( self, userOwner, @@ -452,6 +455,7 @@ def getDataFormPreview( class GetRegistrySectionView(privateView): + validators = (ProjectExistsValidator,) def processView(self): activeProjectUser = self.request.matchdict["user"] @@ -512,6 +516,8 @@ def createDocumentForm( class ChangeProjectMainLanguage_view(privateView): + validators = (ProjectExistsValidator,) + def processView(self): self.returnRawViewResult = True diff --git a/climmob/views/validators/ProjectExistsValidator.py b/climmob/views/validators/ProjectExistsValidator.py index a6b15224..a2eec595 100644 --- a/climmob/views/validators/ProjectExistsValidator.py +++ b/climmob/views/validators/ProjectExistsValidator.py @@ -1,7 +1,7 @@ # TODO Move file to validators/project import json -from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden from climmob.processes import projectExists from climmob.views.classes import privateView, apiView @@ -16,14 +16,17 @@ def __init__(self, view): self.extract() def extract(self): + if issubclass(self.view.__class__, privateView): self.project_owner_username = self.view.request.user self.project_cod = self.view.request.project + self.is_project_close() elif issubclass(self.view.__class__, apiView): body = json.loads(self.view.body) self.project_owner_username = body["user_owner"] self.project_cod = body["project_cod"] + self.is_project_close() else: raise TypeError @@ -37,3 +40,10 @@ def run(self): self.view.request, ): raise HTTPNotFound(self._("There is no a project with that code.")) + + def is_project_close(self): + if (self.view.request.method == "POST" or self.view.request.method == "PUT" or + self.view.request.method == "DELETE"): + if self.view.classResult["project_status"] == 3: + self.view.request.method = "GET" + raise HTTPForbidden() \ No newline at end of file From e5a579c81cca4c12e77b4ae08de1b2a59fb36b76 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Fri, 8 Aug 2025 14:53:10 -0600 Subject: [PATCH 03/29] #327 changes on the way to hidde the info --- climmob/config/routes.py | 2 +- climmob/processes/db/project.py | 23 +-- climmob/templates/dashboard/dashboard.jinja2 | 14 +- .../project/editData/editData.jinja2 | 10 +- .../project/enumerators/enumerators.jinja2 | 159 +++++++++--------- .../templates/project/finishproject.jinja2 | 5 +- .../project/metadata/metadata.jinja2 | 26 +-- .../templates/project/modifyproject.jinja2 | 11 +- .../project/registry/registry.jinja2 | 2 +- .../project/technologies/technologies.jinja2 | 2 +- climmob/templates/snippets/jstreeForm.jinja2 | 2 +- .../snippets/project/project_form.jinja2 | 17 +- climmob/views/classes.py | 2 - climmob/views/project.py | 32 +--- climmob/views/project_enumerators.py | 1 - .../ActionOnlyForProjectOwnerValidator.py | 27 +++ 16 files changed, 151 insertions(+), 184 deletions(-) create mode 100644 climmob/views/validators/ActionOnlyForProjectOwnerValidator.py diff --git a/climmob/config/routes.py b/climmob/config/routes.py index c92af259..5fa0f5a4 100644 --- a/climmob/config/routes.py +++ b/climmob/config/routes.py @@ -924,7 +924,7 @@ def loadRoutes(config): routes.append( addRoute( "finishproject", - "/finishproject", + "/user/{user}/project/{project}/finishproject", FinishProjectView, "project/finishproject.jinja2", ) diff --git a/climmob/processes/db/project.py b/climmob/processes/db/project.py index d834e901..fa39e854 100644 --- a/climmob/processes/db/project.py +++ b/climmob/processes/db/project.py @@ -62,7 +62,8 @@ "getProjectFullDetailsById", "getProjectsByUserThatRequireSetup", "update_project_status", - "update_project_finish", + "get_user_access_type_in_project" + ] @@ -946,16 +947,18 @@ def update_project_status(project_id, status, request): request.dbsession.query(Project).filter( Project.project_id == project_id ).update({"project_status": status}) - return True + return True,"" except Exception as e: return False, str(e) -def update_project_finish(request, project_id): - try: - request.dbsession.query(Project).filter( - Project.project_id == project_id - ).update({"project_status": 3}) +def get_user_access_type_in_project(project_id, user, request): + res = mapFromSchema( + request.dbsession.query(userProject.access_type) + .filter(userProject.user_name == user) + .filter(userProject.project_id == project_id) + .first() + ) + if res: + return True, res["access_type"] + return False, "" - return True, "" - except Exception as e: - return False, str(e) \ No newline at end of file diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index f6e6e424..16832714 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -563,7 +563,7 @@ {% if progress.regtotal > 0 %} - {% if activeProject["project_status"] != 3 %} {{ _("View and edit data") }} {% else %} {{ _('view data') }} {% endif %} + {% if activeProject.project_status != 3 %} {{ _("View and edit data") }} {% else %} {{ _('view data') }} {% endif %} {{ _("Download data in .CSV format") }} {{ _("Download data in .XLSX format") }} @@ -701,7 +701,7 @@ {% block rhomis_btns_for_datacollection scoped%} {% if assessment.asstotal > 0 %} - {% if activeProject["project_status"] != 3 %} {{ _("View and edit data") }} {% else %} {{ _('view data') }} {% endif %} + {% if activeProject.project_status != 3 %} {{ _("View and edit data") }} {% else %} {{ _('view data') }} {% endif %} {{ _("Download data in .CSV format") }} {{ _("Download data in .XLSX format") }} @@ -773,7 +773,7 @@ {% endif %} {% endif %} {% endif %} - {% if hasActiveProject and activeProject["project_status"] != 3 %} + {% if hasActiveProject and activeProject.project_status != 3 and activeProject["access_type"] in [1] %}
{{ _("Finish the project") }}
@@ -783,7 +783,7 @@

{{ _('This action will be block all the functions to add or modify the project.') }}

-
@@ -791,12 +791,6 @@
{% endif %} - - - - - - {% block communityLink %} {% endblock communityLink %} diff --git a/climmob/templates/project/editData/editData.jinja2 b/climmob/templates/project/editData/editData.jinja2 index 04dddfea..373e7852 100755 --- a/climmob/templates/project/editData/editData.jinja2 +++ b/climmob/templates/project/editData/editData.jinja2 @@ -26,10 +26,6 @@ {% endblock pageheading %} {% block pagecontent %} - {% set finished=False %} - {% if activeProject["project_status"] == 3 %} - {% set finished=True %} - {% endif %}
@@ -38,7 +34,7 @@
- {% if finished==False %} + {% if activeProject.project_status != [3] %}
{{ _("Edit registration survey data") }}
{% else %}
{{ _("Registration, survey data") }}
@@ -50,7 +46,7 @@ {% if not dataworking.data %} {% if newStructure|length!=0 %} - {% if finished==False %} + {% if activeProject.project_status != [3] %} {{ _("Available fields for edit") }} {% else %} {{ _("Fields for check") }} @@ -76,7 +72,7 @@ {% endfor %}
- {% if finished == False %} + {% if activeProject.project_status != [3] %} {% endif %} diff --git a/climmob/templates/project/enumerators/enumerators.jinja2 b/climmob/templates/project/enumerators/enumerators.jinja2 index 2aad9c8e..e6998b85 100644 --- a/climmob/templates/project/enumerators/enumerators.jinja2 +++ b/climmob/templates/project/enumerators/enumerators.jinja2 @@ -19,17 +19,10 @@ {% block pageheading %} {% set _title= _("Assign field agents") %} {% set _linkWiki="https://climmob.net/blog/wiki/assign-field-agents/" %} - {% include 'snippets/menuheading.jinja2'%} + {% include 'snippets/menuheading.jinja2' %} {% endblock %} {% block pagecontent %} - {% set finished = False %} - {% if project_status == 3 %} - {% set finished = True %} - {% endif %} - {% if finished %} -
- {% endif %}
@@ -42,58 +35,60 @@
- {% set onlySee = False %} - - {% if activeProject["access_type"] in [4] %} + {% set onlySee = False %}} + {% if activeProject["access_type"] in [4] or activeProject.project_status == 3 %} {% set onlySee = True %} {% endif %} - {% if not onlySee %} + {% if not onlySee %}
{{ _("Assign field agents to the project") }}
- {% if not finished %} - - {% endif %} + + +
{% if error_summary %} {% for key, error in error_summary.items() %}
- + {{ error }}
{% endfor %} {% endif %} {% if enumerators %} -
+ - {% if not finished %} - - {% endif %} + {% for enumerator in enumerators %} - {% if not finished %} - - {% endif %} - - + @@ -101,19 +96,22 @@ {% endfor %}
{{ _('Select to add') }}{{ _('Select to add') }} {{ _('Field agent') }}
- - -
+
+ + +

{{ enumerator.enum_name }}

-

 {{ _("by") }} {{ enumerator.user_fullname[0:12] }}{% if enumerator.user_fullname|length > 12 %}...{% endif %}

+

 {{ _("by") }} {{ enumerator.user_fullname[0:12] }} + {% if enumerator.user_fullname|length > 12 %} + ...{% endif %}

- {% if not finished %} -
- -
- {% endif %} - +
+ +
{% else %}
{% if enumeratorsInProject %} -

{{ _("Click to") }} {{ _("+ Add new field agent")}} {{ _("to register more of them.") }}

- {% else%} -

{{ _("No field agents found. Register your field agents first. Go to Library (left panel) > Field agents.") }}

+

{{ _("Click to") }} {{ _("+ Add new field agent") }} {{ _("to register more of them.") }} +

+ {% else %} +

+ {{ _("No field agents found. Register your field agents first. Go to Library (left panel) > Field agents.") }} +

{% endif %}
@@ -126,7 +124,8 @@
- {% block assigned_title %}
{{ _("Field agents assigned to the project") }}
{% endblock assigned_title %} + {% block assigned_title %} +
{{ _("Field agents assigned to the project") }}
{% endblock assigned_title %}
@@ -135,44 +134,54 @@ {% if enumeratorsInProject %} - - - {% if not (onlySee or finished) %} - - {% endif %} - - - {% for enumeratorParent in enumeratorsInProject %} - {% for enumerator in enumeratorsInProject[enumeratorParent] %} - - + + {% if not (onlySee) %} + + {% endif %} + + + {% for enumeratorParent in enumeratorsInProject %} + {% for enumerator in enumeratorsInProject[enumeratorParent] %} + + - {% if not (onlySee or finished) %} - {% block prj_enum_actions scoped %} - - {% endblock prj_enum_actions %} - {% endif %} - - {% endfor %} + + {% block prj_enumerator_labels scoped %} + {% if enumerator.enum_active != 1 %} + {{ _("Can not submit data") }} + {% endif %} + {% endblock %} + + {% if not (onlySee) %} + {% block prj_enum_actions scoped %} + + {% endblock prj_enum_actions %} + {% endif %} + {% endfor %} + {% endfor %}
{{ _('Name') }}
-
+
{{ _('Name') }}
+

{{ enumerator.enum_name }}

-

 {{ _("by") }} {{ enumerator.user_fullname[0:12] }}{% if enumerator.user_fullname|length > 12 %}...{% endif %}

+

 {{ _("by") }} {{ enumerator.user_fullname[0:12] }} + {% if enumerator.user_fullname|length > 12 %} + ...{% endif %}

-
- {% block prj_enumerator_labels scoped %} - {% if enumerator.enum_active != 1 %} - {{ _("Can not submit data") }} - {% endif %} - {% endblock %} -
- -
+ +
{% else %}
@@ -183,11 +192,9 @@
- {% if finished %} -
- {% endif %} +
diff --git a/climmob/templates/project/modifyproject.jinja2 b/climmob/templates/project/modifyproject.jinja2 index e744f34a..5b856103 100644 --- a/climmob/templates/project/modifyproject.jinja2 +++ b/climmob/templates/project/modifyproject.jinja2 @@ -54,16 +54,11 @@ {% block newprjform %} - {% set finished=False %} - {% if activeProject["project_status"] == 3 %} - {% set finished=True %} - {% endif %} - {% set edit=True%} - {% if activeProject.access_type != 4%} - {% set permissionForChanges=True%} - {% else %} + {% set permissionForChanges=True %} + {% if activeProject.access_type == 4 or activeProject.project_status == 3%} {% set permissionForChanges=False%} + {% endif %} {% set dataworking=data %} {% set allowTemplate=True %} diff --git a/climmob/templates/project/registry/registry.jinja2 b/climmob/templates/project/registry/registry.jinja2 index 871a8db8..ea15be7d 100644 --- a/climmob/templates/project/registry/registry.jinja2 +++ b/climmob/templates/project/registry/registry.jinja2 @@ -55,7 +55,7 @@ {% set onlySee = False %} - {% if activeProject["access_type"] == 4 or activeProject["project_regstatus"] > 0 %} + {% if activeProject["access_type"] == 4 or activeProject["project_regstatus"] > 0 or activeProject["project_status"] == 3 %} {% set onlySee = True %} {% endif %} diff --git a/climmob/templates/project/technologies/technologies.jinja2 b/climmob/templates/project/technologies/technologies.jinja2 index a05f6463..921fff32 100644 --- a/climmob/templates/project/technologies/technologies.jinja2 +++ b/climmob/templates/project/technologies/technologies.jinja2 @@ -37,7 +37,7 @@ {% set onlySee = False %} - {% if activeProject["access_type"] in [4] or activeProject["project_regstatus"] > 0 %} + {% if activeProject["access_type"] in [4] or activeProject["project_regstatus"] > 0 %} {% set onlySee = True %} {% endif %} diff --git a/climmob/templates/snippets/jstreeForm.jinja2 b/climmob/templates/snippets/jstreeForm.jinja2 index 641907dc..0143313f 100644 --- a/climmob/templates/snippets/jstreeForm.jinja2 +++ b/climmob/templates/snippets/jstreeForm.jinja2 @@ -11,7 +11,7 @@ {% endif %} {% if item.createGRP %} -
  • {{ _("Section") }}: {{ item.section_name }} +
  • {{ _("Section") }}: {{ item.section_name }} {% if item.hasQuestions %}
      {% set question=item %} diff --git a/climmob/templates/snippets/project/project_form.jinja2 b/climmob/templates/snippets/project/project_form.jinja2 index f7e054ef..a41f0414 100644 --- a/climmob/templates/snippets/project/project_form.jinja2 +++ b/climmob/templates/snippets/project/project_form.jinja2 @@ -1,7 +1,3 @@ -{% if finished %} -
      -{% endif %} -
      @@ -373,9 +369,7 @@
      -{% if finished %} -
      -{% endif %} + diff --git a/climmob/views/classes.py b/climmob/views/classes.py index 4b3803ea..46dda0ae 100644 --- a/climmob/views/classes.py +++ b/climmob/views/classes.py @@ -364,7 +364,6 @@ def __init__(self, request): "showHelp": False, "showRememberAfterCreateProject": False, "surveyMustBeDisplayed": None, - "project_status": None, } self.viewResult = {} @@ -392,7 +391,6 @@ def __call__(self): if activeProjectData: self.classResult["hasActiveProject"] = True self.classResult["activeProject"] = activeProjectData["project_id"] - self.classResult["project_status"] = activeProjectData["project_status"] else: self.classResult["hasActiveProject"] = False diff --git a/climmob/views/project.py b/climmob/views/project.py index 28cd3570..258203e3 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -51,9 +51,10 @@ get_location_unit_of_analysis_objectives_by_combination, delete_all_project_location_unit_objective, get_all_affiliations, - update_project_finish + update_project_status ) from climmob.views.classes import privateView +from climmob.views.validators.ActionOnlyForProjectOwnerValidator import ActionOnlyForProjectOwnerValidator from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator @@ -944,42 +945,27 @@ def processView(self): class FinishProjectView(privateView): - validator = (ProjectExistsValidator,) + validators = (ProjectExistsValidator, + ActionOnlyForProjectOwnerValidator) def get(self): - user = self.request.GET.get('user') - project = self.request.GET.get('project') - project_info= getActiveProject(user, self.request) + project_info= getActiveProject(self.user.login, self.request) return { - 'user': user, - 'project': project, 'project_info': project_info, } def post(self): - user = self.request.POST.get('user') - project = self.request.POST.get('project') + success, error_update = update_project_status(self.context.active_project_id, 3, self.request) + project_info = getActiveProject(self.user.login, self.request) - success, error_update = update_project_finish(self.request, project) - project_info = getActiveProject(user, self.request) if success: #todo check the persson to send the email - - - self._redirect = HTTPFound(location=self.request.route_url('finishproject')) - return { - 'success': True, - 'user': user, - 'project': project, - 'project_info': project_info, - } + self.returnRawViewResult = True + return HTTPFound(location=self.request.route_url('dashboard')) else: - self._redirect = HTTPFound(location=self.request.route_url('finishproject')) return{ 'error': error_update, - 'user': user, - 'project': project, 'project_info': project_info, } diff --git a/climmob/views/project_enumerators.py b/climmob/views/project_enumerators.py index 93a77bcf..59c2e7a4 100644 --- a/climmob/views/project_enumerators.py +++ b/climmob/views/project_enumerators.py @@ -41,7 +41,6 @@ def get(self): _query={ "user": activeProjectUser, "project": activeProjectCod, - "project_status": self.classResult["project_status"], }, ) ) diff --git a/climmob/views/validators/ActionOnlyForProjectOwnerValidator.py b/climmob/views/validators/ActionOnlyForProjectOwnerValidator.py new file mode 100644 index 00000000..241a1157 --- /dev/null +++ b/climmob/views/validators/ActionOnlyForProjectOwnerValidator.py @@ -0,0 +1,27 @@ + +from pyramid.httpexceptions import HTTPForbidden + +from climmob.views.classes import privateView +from climmob.views.validators.BaseValidator import BaseValidator +from climmob.processes import get_user_access_type_in_project +from climmob.utility.project import ProjectAccessType + +class ActionOnlyForProjectOwnerValidator(BaseValidator): + def __init__(self, view): + super().__init__(view) + + self.project_id = None + self.extract() + + def extract(self): + if issubclass(self.view.__class__, privateView): + self.project_id = self.view.context.active_project_id + else: + raise TypeError + + def run(self): + valid, access_type = get_user_access_type_in_project( + self.project_id, self.view.user.login, self.view.request) + + if not valid or access_type not in [ProjectAccessType.OWNER.value]: + raise HTTPForbidden("This action is forbidden") \ No newline at end of file From 31223748b3e8485b8fe02fdc9f5d4ec2042d8b7f Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Wed, 13 Aug 2025 09:58:50 -0600 Subject: [PATCH 04/29] #327 changes on the way to hidde the info --- climmob/config/routes.py | 12 +- climmob/templates/dashboard/dashboard.jinja2 | 1058 ++++++++++------- .../project/assessment/assessment.jinja2 | 2 +- .../project/editData/editData.jinja2 | 6 +- .../project/enumerators/enumerators.jinja2 | 4 +- .../templates/project/finishproject.jinja2 | 108 +- climmob/templates/project/share/share.jinja2 | 229 ++-- .../project/technologies/technologies.jinja2 | 2 +- .../project/assessment/assessmentform.jinja2 | 42 +- .../project/metadata/metadata_inputs.jinja2 | 74 +- .../project/metadata/template_form.jinja2 | 4 +- .../project/metadata/template_table.jinja2 | 263 ++-- .../test_views_api_project_creation.py | 50 +- .../tests/test_utils/test_views_classes.py | 2 +- .../tests/test_utils/test_views_enumerator.py | 25 + .../tests/test_utils/test_views_project.py | 47 + .../test_views_project_technologies.py | 334 ++++++ .../tests/test_utils/test_views_validators.py | 84 +- climmob/views/Api/projectCreation.py | 11 - climmob/views/classes.py | 2 + climmob/views/editData.py | 162 ++- climmob/views/enumerator.py | 2 +- climmob/views/project.py | 3 +- climmob/views/project_enumerators.py | 2 +- climmob/views/project_metadata.py | 37 +- climmob/views/project_technologies.py | 4 +- .../ActionOnlyForProjectOwnerValidator.py | 4 +- .../validators/ProjectExistsValidator.py | 5 +- 28 files changed, 1649 insertions(+), 929 deletions(-) create mode 100644 climmob/tests/test_utils/test_views_enumerator.py create mode 100644 climmob/tests/test_utils/test_views_project_technologies.py diff --git a/climmob/config/routes.py b/climmob/config/routes.py index 5fa0f5a4..90eb4e40 100644 --- a/climmob/config/routes.py +++ b/climmob/config/routes.py @@ -177,7 +177,7 @@ downloadErroLogDocument_view, ) from climmob.views.enumerator import ( - getEnumeratorDetails_view, + GetEnumeratorDetailsView, enumerators_view, deleteEnumerator_view, ) @@ -219,10 +219,10 @@ from climmob.views.project_analysis import analysisDataView from climmob.views.project_combinations import projectCombinations_view from climmob.views.project_enumerators import ( - projectEnumerators_view, + ProjectEnumeratorsView, removeProjectEnumerators_view, ) -from climmob.views.project_technologies import projectTecnologies_view +from climmob.views.project_technologies import ProjectTechnologiesView from climmob.views.question import ( qlibrary_view, getUserQuestionDetails_view, @@ -710,7 +710,7 @@ def loadRoutes(config): addRoute( "getEnumeratorDetails", "/user/{user}/enumerator/{enumid}", - getEnumeratorDetails_view, + GetEnumeratorDetailsView, "json", ) ) @@ -842,7 +842,7 @@ def loadRoutes(config): addRoute( "prjenumerators", "/user/{user}/project/{project}/enumerators", - projectEnumerators_view, + ProjectEnumeratorsView, "project/enumerators/enumerators.jinja2", ) ) @@ -1034,7 +1034,7 @@ def loadRoutes(config): addRoute( "prjtechnologies", "/user/{user}/project/{project}/technologies", - projectTecnologies_view, + ProjectTechnologiesView, "project/technologies/technologies.jinja2", ) ) diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index 16832714..0c8f5dc9 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -42,7 +42,7 @@ {% include "snippets/maze_loader.jinja2" %} {% if request.registry.settings.get('session.show_expired_modal', "false") == 'true' %} - {% include 'snippets/expired_session.jinja2' %} + {% include 'snippets/expired_session.jinja2' %} {% endif %} {% set withsidebar = [] %} @@ -54,6 +54,9 @@ fill: white; } + {% for msg in request.session.pop_flash('success') %} +
      {{ msg }}
      + {% endfor %}
      {% block mainavbar %}
    • - {{ _('Flag the project as finalized ') }} {{ _('so it can be included in aggregated reports and the 1000FARMS Data Warehouse.).') }} + {{ _('Flag the project as finalized ') }} {{ _('so it can be included in aggregated reports and the 1000FARMS Data Warehouse).') }}
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {{ _('Field Name') }}{{ _('Value') }}
    {{ _('Project ID') }}{{ project_info["project_id"] }}
    {{ _('Project Name') }}{{ project_info["project_name"] }}
    {{ _('Project Code') }}{{ project_info["project_cod"] }}
    {{ _('Project Abstract') }}{{ project_info["project_abstract"] }}
    {{ _('Tags') }}{{ project_info["project_tags"] }}
    {{ _('Person in Charge') }}{{ project_info["project_pi"] }}
    {{ _('Email Person in Charge') }}{{ project_info["project_piemail"] }}
    {{ _('Owner of the project') }}{{ project_info["owner"]["user_name"] }}
    {{ _('Country') }}{{ project_info["project_cnty"] }}
    {{ _('Crop Name') }}{{ project_info["project_curated_cropname"] }}
    {{ _('Affiliation') }}{{ project_info["project_affiliation"] }}
    {{ _('Creation date') }}{{ project_info["project_creationdate"] }}
    {{ _('Project Status') }} - {% if project_info["project_status"] == 0 %} - {{ _("Undefined") }} - {% elif project_info["project_status"] == 1 %} - {{ _("Definition") }} - {% elif project_info["project_status"] == 2 %} - {{ _("In progress") }} - {% elif project_info["project_status"] == 3 %} - {{ _("Finalized") }} - {% endif %} -
    {{ _('Project Public') }} - {% if project_info["project_public"] == 0 %} - {{ _("No") }} - {% elif project_info["project_public"] == 1 %} - {{ _("Yes") }} - {% endif %} -
    - +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{ _('Field Name') }}{{ _('Value') }}
    {{ _('Project Name') }}{{ project_info["project_name"] }}
    {{ _('Project Code') }}{{ project_info["project_cod"] }}
    {{ _('Project Principal investigation') }}{{ project_info["project_pi"] }}
    {{ _('Project owner') }}{{ project_info["owner"]["user_name"] }}
    {{ _('Country') }}{{ project_info["project_cnty"] }}
    {{ _('Crop name') }}{{ project_info["project_curated_cropname"] }}
    {{ _('Affiliation') }}{{ project_info["project_affiliation"] }}
    {{ _('Creation date') }}{{ project_info["project_creationdate"] }}
    {{ _('Project Status') }} + {% if project_info["project_status"] == 0 %} + {{ _("Undefined") }} + {% elif project_info["project_status"] == 1 %} + {{ _("Definition") }} + {% elif project_info["project_status"] == 2 %} + {{ _("In progress") }} + {% elif project_info["project_status"] == 3 %} + {{ _("Finalized") }} + {% endif %} +
    +
    +

    {{ _('If you are sure about it please, press the button.') }}

    diff --git a/climmob/views/project.py b/climmob/views/project.py index 70bba016..9ec9676c 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -983,7 +983,7 @@ def post(self): project_info = getActiveProject(self.user.login, self.request) if success: - self.send_email_notification(project_info) + # self.send_email_notification(project_info) self.returnRawViewResult = True self.request.session.flash( self._("The project was finalized successfully.") From a9b8f41d7b145a5544c012e19c094a54085b6837 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Thu, 11 Sep 2025 16:28:46 -0600 Subject: [PATCH 13/29] edit proyect advance --- climmob/templates/dashboard/dashboard.jinja2 | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index bec39204..2775178e 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -322,11 +322,20 @@
    {{ _("Project definition") }}
    -
    {{ pcompleted }}% {{ _("Completed") }}
    +
    + {% if can_edit %} {{ pcompleted }}% {{ _("Completed") }} + {% else %}{{ _("Finalized") }} + + {% endif %}
    + {% if can_edit %}
    -
    -
    +
    +
  • + {% endif %} @@ -1018,7 +1027,6 @@

    {{ _('If you are certain that your project is complete, press the button below to finalize it.') }}

    -{#

    {{ _('This action will permanently block any further modifications to the project’s data, profile, or metadata.') }}

    #}
    - {% if can_edit %} + {% endif %} +
    +{# {% if can_edit %}#}
    -
    +
    - {% endif %} +
    +{# {% endif %}#} @@ -1019,16 +1021,18 @@
    {{ _("Project finalization.") }}
    - - +
    + + + +
    +

    - {{ _('If you are certain that your project is complete, press the button below to finalize it.') }} + {{ _("A project is considered finalized once it has achieved its objectives, all planned phases have been completed, and no further data entries, profile updates, or metadata changes are expected.") }}

    -

    - {{ _('By finalizing this project, you confirm that it has reached completion and is ready for inclusion in the 1000FARMS Data Warehouse. This action will:') }} + {{ _('By finalizing this project, you confirm that it has reached completion. This action will permanently lock all data entries, profile settings, and metadata, so no further edits will be allowed. Please review the project details carefully before proceeding. + + +') }}

    -
      -
    • - {{ _('Permanently lock ') }} {{ _('all data entries, profile settings, and metadata (no further edits will be possible).') }} -
    • -
    -
      -
    • - {{ _('Flag the project as finalized ') }} {{ _('so it can be included in aggregated reports and the 1000FARMS Data Warehouse).') }} -
    • -
    +
    @@ -70,30 +64,30 @@ {{ _('Project Name') }} {{ project_info["project_name"] }} - - {{ _('Project Code') }} - {{ project_info["project_cod"] }} - - - {{ _('Project Principal investigation') }} - {{ project_info["project_pi"] }} - - - {{ _('Project owner') }} - {{ project_info["owner"]["user_name"] }} - - - {{ _('Country') }} - {{ project_info["project_cnty"] }} - - - {{ _('Crop name') }} - {{ project_info["project_curated_cropname"] }} - - - {{ _('Affiliation') }} - {{ project_info["project_affiliation"] }} - +{# #} +{# {{ _('Project Code') }}#} +{# {{ project_info["project_cod"] }}#} +{# #} +{# #} +{# {{ _('Project Principal investigation') }}#} +{# {{ project_info["project_pi"] }}#} +{# #} +{# #} +{# {{ _('Project owner') }}#} +{# {{ project_info["owner"]["user_name"] }}#} +{# #} +{# #} +{# {{ _('Country') }}#} +{# {{ project_info["project_cnty"] }}#} +{# #} +{# #} +{# {{ _('Crop name') }}#} +{# {{ project_info["project_curated_cropname"] }}#} +{# #} +{# #} +{# {{ _('Affiliation') }}#} +{# {{ project_info["project_affiliation"] }}#} +{# #} {{ _('Creation date') }} {{ project_info["project_creationdate"] }} @@ -120,7 +114,7 @@ {{ _('If you are sure about it please, press the button.') }}

    {{ _('Confirm finalization of this project') }} From 333c4b503f714b5e4e7e55decebe59ca9333cf47 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Mon, 10 Nov 2025 13:39:11 -0600 Subject: [PATCH 16/29] Labels changes --- climmob/templates/dashboard/dashboard.jinja2 | 40 ++++++--- climmob/templates/email/close_project.jinja2 | 60 +++++++++---- .../templates/project/finishproject.jinja2 | 87 +++++++++++-------- climmob/utility/email.py | 16 ---- climmob/views/project.py | 2 +- 5 files changed, 120 insertions(+), 85 deletions(-) diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index ebfd28b7..f89ed480 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -306,6 +306,30 @@ {% endif %} {% if hasActiveProject %} + {% if not can_edit %} +
    + +
    +

    The project has been successfully completed.

    +
    +
    + {% endif %} + +
    @@ -320,22 +344,14 @@
    {{ _("Project definition") }}
    -
    - {{ pcompleted }}% {% if can_edit %} {{ _("Completed") }} - {% else %}{{ _("Project set as finalized") }} - - {% endif %} +
    + {{ pcompleted }}% {{ _("Completed") }}
    -{# {% if can_edit %}#}
    -{# {% endif %}#} @@ -1032,8 +1048,8 @@

    diff --git a/climmob/templates/email/close_project.jinja2 b/climmob/templates/email/close_project.jinja2 index a794b0e4..07f716dc 100644 --- a/climmob/templates/email/close_project.jinja2 +++ b/climmob/templates/email/close_project.jinja2 @@ -39,27 +39,49 @@ {{ project_info["project_cod"] }} {{ _('was officially finalized today, ') }} {{ date }}.

    - {{ _('From this point forward, all project-related tasks, data entries, and activities are permanently locked. Thank you for your support in bringing this project to successful completion.') }} + {{ _('From this point forward, all project-related tasks, data entries, and activities are permanently locked.') }}

    -

    {{ _('The project details are as follows::') }}

    -

    {{ _('Project Name') }}: {{ project_info["project_name"] }}

    +

    {{ _('The project details are as follows:') }}

    +
    +
      {{ _('Project name') }}: {{ project_info["project_name"] }}
    {#

    {{ _('Project Code') }}: {{ project_info["project_cod"] }}

    #} -{#

    {{ _('Project Principal investigation') }}: {{ project_info["project_pi"] }}

    #} -{#

    {{ _('Project owner') }}: {{ project_info["owner"]["user_name"] }}

    #} -{#

    {{ _('Country') }}: {{ project_info["project_cnty"] }}

    #} -{#

    {{ _('Crop name') }}: {{ project_info["project_curated_cropname"] }}

    #} -{#

    {{ _('Affiliation') }}: {{ project_info["project_affiliation"] }}

    #} -

    {{ _('Creation date') }}: {{ project_info["project_creationdate"] }}

    -

    {{ _('Project Status') }}: {% if project_info["project_status"] == 0 %} - {{ _("Undefined") }} - {% elif project_info["project_status"] == 1 %} - {{ _("Definition") }} - {% elif project_info["project_status"] == 2 %} - {{ _("In progress") }} - {% elif project_info["project_status"] == 3 %} - {{ _("Finalized") }} - {% endif %}

    -

    {{ _('Thank you for your continued collaboration.') }}

    +
      {{ _('Project Principal investigation') }}: {{ project_info["project_pi"] }}
    +
      {{ _('Owner') }}: {{ project_info["owner"]["user_name"] }}
    +
      {{ _('Country') }}: {{ project_info["project_cnty"] }}
    +
      {{ _('Crop name') }}: {{ project_info["project_curated_cropname"] }}
    +{#

    {{ _('Affiliation') }}: {{ project_info["project_affiliation"] }}{{ _('Creation date') }}: {{ project_info["project_creationdate"] }} +

      {{ _('Progress') }}: + + {% if project_info.project_regstatus == 0 %} + {{ _("Registration not yet started") }} + {% else %} + {% if project_info.project_regstatus == 1 %} + {{ _("Registration ongoing") }} + {% else %} + {% if project_info.project_regstatus == 2 %} + {% if project_info.project_assstatus == 0 %} + {{ _("Registration closed") }} + {% else %} + {{ _("Trial data collection / Analysis ongoing") }} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + +
    +
      {{ _('Status') }}: + {% if project_info["project_status"] == 0 %} + {{ _("Undefined") }} + {% elif project_info["project_status"] == 1 %} + {{ _("Definition") }} + {% elif project_info["project_status"] == 2 %} + {{ _("In progress") }} + {% elif project_info["project_status"] == 3 %} + {{ _("Finalized") }} + {% endif %}
    +
    +

    {{ _('Thank you for collaborating in the 1000FARMS Network monitoring!') }}

    {% endblock email_message %} {% block email_goodbye %} diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 616a8b42..0b4628b3 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -13,9 +13,9 @@ {% block css %} {% cssresource request,'coreresources','sweet' %} {% endblock css %} @@ -34,64 +34,72 @@ {% set can_edit = False %} {% endif %} +{% set progress_status %} + {% if project_info.project_regstatus == 0 %} + {{ _("Registration not yet started") }} + {% elif project_info.project_regstatus == 1 %} + {{ _("Registration ongoing") }} + {% elif project_info.project_regstatus == 2 %} + {% if project_info.project_assstatus == 0 %} + {{ _("Registration closed") }} + {% else %} + {{ _("Trial data collection / Analysis ongoing") }} + {% endif %} + {% endif %} +{% endset %} + {% if can_edit %}
    {{ _('Confirmation details') }}
    {{ _('Warning') }} + class="label label-warning">{{ _('Warning') }}

    {{ _('By finalizing this project, you confirm that it has reached completion. This action will permanently lock all data entries, profile settings, and metadata, so no further edits will be allowed. Please review the project details carefully before proceeding. - ') }}

    -
    -
    - + - + -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} + + + + +
    {{ _('Field Name') }}{{ _('Field name') }} {{ _('Value') }}
    {{ _('Project Name') }}{{ _('Project name') }} {{ project_info["project_name"] }}
    {{ _('Project Code') }}{{ project_info["project_cod"] }}
    {{ _('Project Principal investigation') }}{{ project_info["project_pi"] }}
    {{ _('Project owner') }}{{ project_info["owner"]["user_name"] }}
    {{ _('Country') }}{{ project_info["project_cnty"] }}
    {{ _('Crop name') }}{{ project_info["project_curated_cropname"] }}
    {{ _('Affiliation') }}{{ project_info["project_affiliation"] }}
    {{ _('Creation date') }} {{ project_info["project_creationdate"] }}
    {{ _('Progress') }} + {% if project_info.project_regstatus == 0 %} + {{ _("Registration not yet started") }} + {% else %} + {% if project_info.project_regstatus == 1 %} + {{ _("Registration ongoing") }} + {% else %} + {% if project_info.project_regstatus == 2 %} + {% if project_info.project_assstatus == 0 %} + {{ _("Registration closed") }} + {% else %} + {{ _("Trial data collection / Analysis ongoing") }} + {% endif %} + {% endif %} + {% endif %} + {% endif %} +
    {{ _('Project Status') }} @@ -113,13 +121,18 @@

    {{ _('If you are sure about it please, press the button.') }}

    - - + {{ _('Confirm finalization of this project') }} - diff --git a/climmob/utility/email.py b/climmob/utility/email.py index 99fd4a2f..e7ee23ec 100644 --- a/climmob/utility/email.py +++ b/climmob/utility/email.py @@ -47,22 +47,6 @@ def build_email_message_multiple_recipients(body, subject, recipients, mail_from return msg - -def build_email_message_multiple_recipients(body, subject, recipients, mail_from): - """ - recipients: List of tuples: [(name1, email1), (name2, email2), ...] - """ - msg = MIMEText(body.encode("utf-8"), "html", "utf-8") - msg["Subject"] = Header(subject.encode("utf-8"), "utf-8") - msg["From"] = "ClimMob <{}>".format(mail_from) - - to_header = ", ".join(["{} <{}>".format(name, email) for name, email in recipients]) - msg["To"] = to_header - msg["Date"] = utils.formatdate(time()) - - return msg - - class EmailSender: def __init__(self, settings): self.smtp_server = settings.get("email.server", "localhost") diff --git a/climmob/views/project.py b/climmob/views/project.py index fcacd181..bc6e60a5 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -984,7 +984,7 @@ def post(self): self.send_email_notification(project_info) self.returnRawViewResult = True self.request.session.flash( - self._("The project was finalized successfully.") + self._("The project has been successfully finalized. Thank you for your dedication!. Congratulations!!!") ) return HTTPFound(location=self.request.route_url("dashboard")) else: From e7f31befb8e2bdb04e83ec468094becfb5a86e5f Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Tue, 11 Nov 2025 14:43:57 -0600 Subject: [PATCH 17/29] columns crop & country added to all projects --- climmob/templates/project/finishproject.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 0b4628b3..25b31a37 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -122,7 +122,7 @@ {{ _('If you are sure about it please, press the button.') }}

    -

    The project has been successfully completed.

    +

    The project has been successfully finalized.

    {% endif %} From 0785ae4c1a867a44e6251beef0dc2d874e82d4e5 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Thu, 19 Feb 2026 10:03:47 -0600 Subject: [PATCH 19/29] Hide close button when the proyect is fin modified the message according to the req modifications on the email warning tag on the finish page when the metadata isnt complete set the project selected when you access to the finish page button added on the proyect list to finish it validation test for the changes --- climmob/templates/dashboard/dashboard.jinja2 | 8 +-- climmob/templates/email/close_project.jinja2 | 5 +- .../project/assessment/assessment.jinja2 | 2 +- .../templates/project/finishproject.jinja2 | 29 +++++++- climmob/templates/project/projectList.jinja2 | 30 ++++++++- .../tests/test_utils/test_views_project.py | 67 ++++++++++++++++--- .../tests/test_utils/test_views_validators.py | 4 +- climmob/views/cleanErrorLogs.py | 2 +- climmob/views/project.py | 43 ++++++++---- climmob/views/project_combinations.py | 2 +- .../project/ProjectOpenValidator.py | 4 +- 11 files changed, 155 insertions(+), 41 deletions(-) diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index c20d8166..56a8d245 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -311,7 +311,7 @@
    - Project Finalized + {{ _("Project Finalized") }}
    @@ -325,7 +325,7 @@
    -

    The project has been successfully finalized.

    +

    {{ _("The project has been successfully finalized") }}.

    {% endif %} @@ -1035,7 +1035,7 @@ {% if hasActiveProject and can_edit and activeProject["access_type"] in [1] and activeUser.login == activeProject.owner.user_name %}
    -
    {{ _("Project finalization.") }}
    +
    {{ _("Project finalization") }}
    @@ -1045,7 +1045,7 @@

    - {{ _("A project is considered finalized once it has achieved its objectives, all planned phases have been completed, and no further data entries, profile updates, or metadata changes are expected.") }} + {{ _("Finalizing a project is recommended once all objectives have been met and all planned phases are complete. Please ensure that no further data entries, profile updates, or metadata changes are required before proceeding.") }}

    + {% if not onlySee %} {% endif %} {% if assessment.ass_status == 0 %} diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 25b31a37..c26faf24 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -57,10 +57,33 @@

    - {{ _('By finalizing this project, you confirm that it has reached completion. This action will permanently lock all data entries, profile settings, and metadata, so no further edits will be allowed. Please review the project details carefully before proceeding. - -') }} + {{ _('By finalizing this project, you confirm that it has reached completion. This action will permanently lock all data entries, profile settings, and metadata, so no further edits will be allowed. Please review the project details carefully before proceeding.') }}

    + {% set total_ass_records = 0 %} + {% for assessment in progress["assessments"] %} + {% if assessment["ass_status"] == 1 or assessment["ass_status"] == 2 %} + {% set total_ass_records = total_ass_records + assessment["asstotal"] %} + {% endif %} + {% endfor %} + {% if (total_ass_records > 5 or (project_info.project_registration_and_analysis == 1 and progress.regtotal >=5)) + and progress.metadata == False %} +
    +
    + + {{ _("Important:")}} + +
    +
    +

    + {{ _("The Analysis module + requires the 'Trial documentation' step to be completed to generate results. If you finalize + while this step is pending, you will be unable to access the Analysis section due to these data + requirements.") }} +

    +
    +
    + {% endif %} +
    diff --git a/climmob/templates/project/projectList.jinja2 b/climmob/templates/project/projectList.jinja2 index 7976d413..62a7c100 100644 --- a/climmob/templates/project/projectList.jinja2 +++ b/climmob/templates/project/projectList.jinja2 @@ -87,6 +87,7 @@ {% set createBoton = false %} {% endif %} +
    @@ -129,13 +130,24 @@ {% if createBoton == true %}
    {% endif %} - + + {% for project in userProjects %} + {% set can_edit = true %} + {% if project.project_status == 3 %} + {% set can_edit = false %} + {% endif %} + + {% set is_owner = false %} + {% if activeUser.login == project.owner.user_name %} + {% set is_owner = True %} + {% endif %} + {# + {% if is_owner %} + {% if can_edit %} + + + {% else %} + + {% endif %} + {% else %} + + {% endif %} {% endfor %} diff --git a/climmob/tests/test_utils/test_views_project.py b/climmob/tests/test_utils/test_views_project.py index f368ea40..e4893211 100644 --- a/climmob/tests/test_utils/test_views_project.py +++ b/climmob/tests/test_utils/test_views_project.py @@ -1615,15 +1615,27 @@ def setUp(self): self.view.context.active_project_id = 1 self.view.request.registry.settings = {"email.from": "email_send@test.com"} - self.view.project_info = {"project_cod": MagicMock(name="fake_code")} - fake_now = datetime(2024, 1, 1, 12, 0, 0) + self.view.project_info = { + "project_cod": MagicMock(name="fake_code"), + "project_id": MagicMock(name="fake_id"), + } + fake_now = datetime(2024, 1, 1, 12, 0, 0).strftime("%Y-%m-%d %H:%M:%S") self.view.request.translate = lambda text: text + self.get_project_id_patcher = patch( + "climmob.views.project.getTheProjectIdForOwner" + ) + self.set_active_project_patcher = patch( + "climmob.views.project.setActiveProject" + ) self.active_project_patcher = patch("climmob.views.project.getActiveProject") self.update_project_status_patcher = patch( "climmob.views.project.update_project_status" ) self.get_all_user_admin_patcher = patch("climmob.views.project.getAllUserAdmin") + self.get_project_progress_patcher = patch( + "climmob.views.project.getProjectProgress" + ) self.render_template_patcher = patch("climmob.views.project.render_template") self.datetime_patcher = patch("climmob.views.project.datetime.datetime") self.build_email_message_multiple_recipients_patcher = patch( @@ -1632,29 +1644,37 @@ def setUp(self): self.email_sender_patcher = patch("climmob.views.project.EmailSender") self.log_patcher = patch("climmob.views.project.log") + self.mock_get_project_id = self.get_project_id_patcher.start() + self.mock_set_active_project = self.set_active_project_patcher.start() self.mock_project_info = self.active_project_patcher.start() + self.mock_progress = self.get_project_progress_patcher.start() self.mock_success = self.update_project_status_patcher.start() self.mock_admin_users_patcher = self.get_all_user_admin_patcher.start() self.mock_text = self.render_template_patcher.start() self.mock_datetime = self.datetime_patcher.start() self.mock_msg = self.build_email_message_multiple_recipients_patcher.start() self.mock_email_sender = self.email_sender_patcher.start() + self.mock_email_sender.return_value.send_email.return_value = None + self.mock_log = self.log_patcher.start() - self.mock_project_info.return_value = {"project_cod": "data"} self.mock_success.return_value = (True, "") self.mock_admin_users_patcher.return_value = [ {"user_fullname": "name1", "user_email": "email1"}, {"user_fullname": "name2", "user_email": "email2"}, ] - self.mock_text.return_value = [ - {"user_fullname": "name1", "user_email": "email1"}, - {"user_fullname": "name2", "user_email": "email2"}, - ] - self.mock_datetime.now.return_value = fake_now + self.mock_text.return_value = "rendered email body" + self.mock_datetime.now.strftime.return_value = fake_now self.mock_msg.return_value = "some text to add to the email body" + self.mock_progress.return_value = {"data_progress": "result_data"}, True + self.view.request.matchdict = {"project": "PRJ001"} + self.mock_get_project_id.return_value = (MagicMock(name="fake_id"),) + + self.addCleanup(self.get_project_id_patcher.stop) + self.addCleanup(self.set_active_project_patcher.stop) self.addCleanup(self.active_project_patcher.stop) + self.addCleanup(self.get_project_progress_patcher.stop) self.addCleanup(self.update_project_status_patcher.stop) self.addCleanup(self.get_all_user_admin_patcher.stop) self.addCleanup(self.render_template_patcher.stop) @@ -1664,6 +1684,18 @@ def setUp(self): self.addCleanup(self.mock_log.stop) def tearDown(self): + if self.mock_get_project_id.called: + self.mock_get_project_id.assert_called_once_with( + self.view.user.login, "PRJ001", self.view.request + ) + + if self.mock_set_active_project.called: + self.mock_set_active_project.assert_called_once_with( + self.view.user.login, + self.mock_get_project_id.return_value, + self.view.request, + ) + if self.mock_project_info.called: self.mock_project_info.assert_called_once_with( self.view.user.login, self.view.request @@ -1678,7 +1710,7 @@ def tearDown(self): self.mock_text.assert_called_once_with( "email/close_project.jinja2", { - "date": self.mock_datetime.now.return_value, + "date": self.mock_datetime.now.return_value.strftime.return_value, "project_info": self.view.project_info, "_": self.view.request.translate, }, @@ -1692,11 +1724,24 @@ def tearDown(self): [("name1", "email1"), ("name2", "email2")], self.view.request.registry.settings["email.from"], ) + if self.mock_progress.called: + self.mock_progress.assert_called_once_with( + self.view.user.login, + self.mock_project_info.return_value["project_cod"], + self.mock_project_info.return_value["project_id"], + self.view.request, + ) super().tearDown() def test_finish_project_view_get(self): response = self.view.get() - self.assertEqual(response, {"project_info": {"project_cod": "data"}}) + self.assertEqual( + response, + { + "project_info": self.mock_project_info.return_value, + "progress": {"data_progress": "result_data"}, + }, + ) @patch.object(FinishProjectView, "send_email_notification", return_value=True) def test_finish_project_view_post_success(self, mock_send_email_notification): @@ -1715,7 +1760,7 @@ def test_finish_project_view_post_fail(self): response, { "error": self.mock_success.return_value[1], - "project_info": {"project_cod": "data"}, + "project_info": self.mock_project_info.return_value, }, ) diff --git a/climmob/tests/test_utils/test_views_validators.py b/climmob/tests/test_utils/test_views_validators.py index 81ab13d6..e1677701 100644 --- a/climmob/tests/test_utils/test_views_validators.py +++ b/climmob/tests/test_utils/test_views_validators.py @@ -806,7 +806,7 @@ def test_is_project_close_invalid(self): self.validator.run() self.assertEqual( str(cm.exception), - "This project has been finalized and can no longer be modified. You do not have access to make changes.", + "This project has been finalized and is now in read-only mode. Modifications are no longer permitted to ensure the integrity of the final data.", ) self.assertEqual(self.view.request.method, "GET") @@ -883,7 +883,7 @@ def test_api_post_method_with_finalized_project_should_raise_forbidden(self): with self.assertRaises(HTTPForbidden) as context: self.validator.run() self.assertEqual( - "This project has been finalized and can no longer be modified. You do not have access to make changes.", + "This project has been finalized and is now in read-only mode. Modifications are no longer permitted to ensure the integrity of the final data.", str(context.exception), ) diff --git a/climmob/views/cleanErrorLogs.py b/climmob/views/cleanErrorLogs.py index 5e3d26ca..48034744 100644 --- a/climmob/views/cleanErrorLogs.py +++ b/climmob/views/cleanErrorLogs.py @@ -45,7 +45,7 @@ def processView(self): if proData["project_status"] == ProjectStatus.FINALIZED.value: raise HTTPForbidden( self._( - "This project has been finalized and can no longer be modified. You do not have access to make changes." + "This project has been finalized and is now in read-only mode. Modifications are no longer permitted to ensure the integrity of the final data." ) ) try: diff --git a/climmob/views/project.py b/climmob/views/project.py index bc6e60a5..96ffbd61 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -54,6 +54,8 @@ get_all_affiliations, update_project_status, getAllUserAdmin, + getProjectProgress, + setActiveProject, ) from climmob.utility.email import ( render_template, @@ -968,9 +970,22 @@ class FinishProjectView(privateView): ) def get(self): + request_activeUSer = self.request.user + request_activeProjectCod = self.request.project + activeProjectId = getTheProjectIdForOwner( + request_activeUSer, request_activeProjectCod, self.request + ) + setActiveProject(self.user.login, activeProjectId, self.request) project_info = getActiveProject(self.user.login, self.request) + progress, pcompleted = getProjectProgress( + self.user.login, + project_info["project_cod"], + project_info["project_id"], + self.request, + ) return { "project_info": project_info, + "progress": progress, } def post(self): @@ -984,7 +999,9 @@ def post(self): self.send_email_notification(project_info) self.returnRawViewResult = True self.request.session.flash( - self._("The project has been successfully finalized. Thank you for your dedication!. Congratulations!!!") + self._( + "The project has been successfully finalized. Thank you for your dedication! Congratulations!" + ) ) return HTTPFound(location=self.request.route_url("dashboard")) else: @@ -1002,15 +1019,13 @@ def send_email_notification(self, project_info): ) return False - # admin_users = getAllUserAdmin(self.request) - recipients = [("Pablo Orozco", "porozco@mrbotcr.com"), - # ("Marilyn Manrow ", "mmanrow@mrbotcr.com") - ] - # for admin_user in admin_users: - # recipients.append((admin_user["user_fullname"], admin_user["user_email"])) - # if not recipients: - # log.warning("Email didn't send. No recipients found.") - # return False + admin_users = getAllUserAdmin(self.request) + recipients = [] + for admin_user in admin_users: + recipients.append((admin_user["user_fullname"], admin_user["user_email"])) + if not recipients: + log.warning("Email didn't send. No recipients found.") + return False subject = ( "✅ Project " + str(project_info["project_cod"]) + " has been finalized" @@ -1018,7 +1033,12 @@ def send_email_notification(self, project_info): try: text = render_template( "email/close_project.jinja2", - {"date": datetime.datetime.now(), "project_info": project_info, "_": _}, + { + "date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "project_info": project_info, + "_": _, + "link" : self.request.route_url("projectsSummaryRecent") + }, ) except Exception as e: log.error(f"Error rendering email template: {e}") @@ -1031,7 +1051,6 @@ def send_email_notification(self, project_info): except Exception as e: log.error(f"Error building email message: {e}") return False - try: recipient_emails = [email for _, email in recipients] email_sender = EmailSender(self.request.registry.settings) diff --git a/climmob/views/project_combinations.py b/climmob/views/project_combinations.py index 5f223330..a2eb7632 100644 --- a/climmob/views/project_combinations.py +++ b/climmob/views/project_combinations.py @@ -61,7 +61,7 @@ def processView(self): ): raise HTTPForbidden( self._( - "This project has been finalized and can no longer be modified. You do not have access to make changes." + "This project has been finalized and is now in read-only mode. Modifications are no longer permitted to ensure the integrity of the final data." ) ) diff --git a/climmob/views/validators/project/ProjectOpenValidator.py b/climmob/views/validators/project/ProjectOpenValidator.py index 10112fce..428ccfee 100644 --- a/climmob/views/validators/project/ProjectOpenValidator.py +++ b/climmob/views/validators/project/ProjectOpenValidator.py @@ -32,7 +32,7 @@ def run(self): self.view.request.method = "GET" raise HTTPForbidden( self._( - "This project has been finalized and can no longer be modified. You do not have access to make changes." + "This project has been finalized and is now in read-only mode. Modifications are no longer permitted to ensure the integrity of the final data." ) ) elif issubclass(self.view.__class__, apiView): @@ -45,7 +45,7 @@ def run(self): if project_status == ProjectStatus.FINALIZED.value: raise HTTPForbidden( self._( - "This project has been finalized and can no longer be modified. You do not have access to make changes." + "This project has been finalized and is now in read-only mode. Modifications are no longer permitted to ensure the integrity of the final data." ) ) From 11a7614b5369cc8c7c7d9d3af1d4b3ce23c4e55e Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Tue, 24 Feb 2026 07:50:19 -0600 Subject: [PATCH 20/29] changes on finalize warning banner to a warning popup. error correction on filters over the table --- .../templates/project/finishproject.jinja2 | 43 +++++++++++-------- climmob/templates/project/projectList.jinja2 | 1 + 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index c26faf24..8344e445 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -6,7 +6,6 @@ {% block pageheading %} {% set _title= _("Confirm project finalization") %} - {% set _linkWiki="https://climmob.net/blog/wiki/cancel-form/" %} {% include 'snippets/menuheading.jinja2' %} {% endblock %} @@ -22,6 +21,7 @@ {% block topScripts %} {% jsresource request,'coreresources','sweet' %} + {% jsresource request,'coreresources','toastr' %} {% endblock topScripts %} {% block pagecontent %} @@ -65,24 +65,6 @@ {% set total_ass_records = total_ass_records + assessment["asstotal"] %} {% endif %} {% endfor %} - {% if (total_ass_records > 5 or (project_info.project_registration_and_analysis == 1 and progress.regtotal >=5)) - and progress.metadata == False %} -
    -
    - - {{ _("Important:")}} - -
    -
    -

    - {{ _("The Analysis module - requires the 'Trial documentation' step to be completed to generate results. If you finalize - while this step is pending, you will be unable to access the Analysis section due to these data - requirements.") }} -

    -
    -
    - {% endif %}
    @@ -182,6 +164,29 @@
    {% endif %} + + {% endblock pagecontent %} {% block script %} diff --git a/climmob/templates/project/projectList.jinja2 b/climmob/templates/project/projectList.jinja2 index 62a7c100..e8e6c9b9 100644 --- a/climmob/templates/project/projectList.jinja2 +++ b/climmob/templates/project/projectList.jinja2 @@ -316,6 +316,7 @@ columnDefs: [ {target: -1, columnControl: [], orderable: false }, {target: -2, columnControl: [], orderable: false }, + {target: -3, columnControl: [], orderable: false }, { targets: hiddenIndexes, visible: false, From 0e8285fce1202dce6efed4a446b7d812e3ae71d2 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Wed, 25 Feb 2026 15:21:14 -0600 Subject: [PATCH 21/29] fix an error about project_registration_and_analysis modification form --- climmob/views/project.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/climmob/views/project.py b/climmob/views/project.py index 96ffbd61..c6536d1c 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -619,6 +619,12 @@ def processView(self): ] = location_unit_of_analysis[ "registration_and_analysis" ] + else: + data[ + "project_registration_and_analysis" + ] = cdata[ + "project_registration_and_analysis" + ] if "usingTemplate" in data.keys(): if data["usingTemplate"] != "": From 10ba76098b7c5c350a402a9781dd63350fc3c254 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Fri, 27 Feb 2026 11:40:44 -0600 Subject: [PATCH 22/29] text edition --- climmob/templates/project/finishproject.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 8344e445..799117e1 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -167,7 +167,7 @@ - {% endif %} {% if hasActiveProject %} - {% if not can_edit %} -
    -
    -
    - {{ _("Project Finalized") }} - - -
    - -
    -
    -

    {{ _("The project has been successfully finalized") }}.

    -
    -
    - {% endif %} - -
    @@ -366,11 +340,13 @@ {{ _("1. Project profile") }} + class="fa fa-list-alt"> {{ _("1. Project profile") }} +

    {{ _("Completed") }}

    + class="label label-success">{{ _("Completed") }} +

    {% if activeProject["project_template"] != 1 %} @@ -383,17 +359,28 @@ onclick="seeHelp()" {% else %} href="{{ request.route_url('prjenumerators',user=activeProject["owner"].user_name, project=activeProject["project_cod"]) }}" {% endif %}> {{ _("2. Assign field agents") }} - {% if progress.numberOfFieldAgents > 0 %} - {{ progress.numberOfFieldAgents }} {{ _("Assigned") }} {% endif %} + {% if progress.numberOfFieldAgents > 0 %} + {{ progress.numberOfFieldAgents }} {{ _("Assigned") }} + + {% endif %}

    {% if progress.enumerators %} - {{ _("Completed") }}{% else %} - {{ _("Validation") }}{% endif %}

    + class="label + {% if progress.enumerators %}label-success + {% elif can_edit %}label-danger + {% else %}label-default{% endif %}"> + {% if progress.enumerators %} + {{ _("Completed") }} + {% elif can_edit %} + {{ _("Validation") }} + {% else %} + {{ _("Incomplete") }} + {% endif %} +

    {% endblock %}
    @@ -407,10 +394,11 @@ disabled {% else %} href="{{ request.route_url('prjtechnologies', user=activeProject["owner"].user_name, project=activeProject["project_cod"]) }}" {% endif %}> {{ _("3. Assign technologies") }} - {% if progress.numberOfCombinations > 0 %} - {{ progress.numberOfCombinations }} {{ _("Assigned") }} {% endif %} + {% if progress.numberOfCombinations > 0 %} + {{ progress.numberOfCombinations }} {{ _("Assigned") }} + {% endif %}
    @@ -420,13 +408,17 @@ {% else %} {% if progress.technology and progress.techalias %} {{ _("Completed") }} - {% endif %} - {% if progress.technology and not progress.techalias %} - {{ _("Partial") }} - {% endif %} - {% if not progress.technology and not progress.techalias %} - {{ _("Pending") }} +{# {% endif %}#} + {% elif can_edit %} + {% if progress.technology and not progress.techalias %} + {{ _("Partial") }} + {% endif %} + {% if not progress.technology and not progress.techalias %} + {{ _("Pending") }} + {% endif %} + {% else %} + {{ _("Incomplete") }} {% endif %} {% endif %}

    @@ -461,8 +453,10 @@

    {% if progress.registry %} {{ _("Completed") }} - {% else %} + {% elif can_edit %} {{ _("Pending") }} + {% else %} + {{ _("Incomplete") }} {% endif %}

    @@ -486,8 +480,10 @@

    {% if progress.assessment %} {{ _("Completed") }} - {% else %} + {% elif can_edit %} {{ _("Pending") }} + {% else %} + {{ _("Incomplete") }} {% endif %}

    @@ -513,8 +509,10 @@

    {% if progress.metadata %} {{ _("Completed") }} - {% else %} + {% elif can_edit %} {{ _("Pending") }} + {% else %} + {{ _("Incomplete") }} {% endif %}

    @@ -561,55 +559,74 @@ {% if activeProject["project_template"] != 1 %} - {% if activeProject.project_registration_and_analysis != 1 and can_edit %} + {% if activeProject.project_registration_and_analysis != 1 %}
    -
    -
    {{ _("Start trial data collection moments") }}
    +
    + {% if can_edit %} + {{ _("Start trial data collection moments") }} + {% else %} + {{ _("Trial data collection moments") }} + {% endif %} +
    - - {% if progress["enumerators"] and progress["technology"] and progress["techalias"] and progress["registry"] and progress["assessment"] and progress.regsubmissions == 2 %} -
    -
    - {% if progress.usableAssessments|length > 0 %} - {% include 'snippets/error.jinja2' %} -
    -
    - - + {% if can_edit %} + {% if progress["enumerators"] and progress["technology"] and progress["techalias"] and progress["registry"] and progress["assessment"] and progress.regsubmissions == 2 %} +
    +
    + {% if progress.usableAssessments|length > 0 %} + {% include 'snippets/error.jinja2' %} + +
    + + +
    + + + + {% else %} +
    +

    {{ _("You already start all the data collection moments") }}

    - - - - {% endif %} -
    - {% else %} -
    -
    -

    {{ _("You can start the trial data collection once the registration of participants is closed and the project definition is completed") }}

    + {% endif %}
    + {% else %} +
    +
    +

    + {% if can_edit %} + {{ _("You can start the trial data collection once the registration of participants is closed and the project definition is completed") }} + {% endif %} +

    +
    +
    + {% endif %} + {% else %} +
    +

    + {{ _("You can not start the trial data collection once the project was finalized") }} +

    {% endif %}
    @@ -634,28 +651,32 @@
    {{ _("Participant registration") }}
    - + {% if progress.regtotal > 0 %} + ({{ ((progress.regtotal * 100)/activeProject.project_numobs)|round(1, 'floor') }}%) + {% endif %} + @@ -757,7 +778,7 @@ {% endblock dataprivacy_download_data_registry %} {% endif %} - {% if progress.regerrors > 0 and progress.regsubmissions == 1 and can_edit %} + {% if progress.regerrors > 0 and progress.regsubmissions == 1 %} - {% if progress.regsubmissions == 1 and can_edit %} + {% if progress.regsubmissions == 1 %} {% if progress.regtotal > 0 %} -
    - -
    -
    - -
    + {% if can_edit %} +
    + +
    +
    + +
    + {% else %} +
    +

    {{ _("Project finalized, for the integrity of the data you can not make changes") }}

    +
    + {% endif %} {% else %} -
    - -
    + {% if can_edit %} +
    + +
    + {% endif %} {% endif %} {% endif %} {% if progress.regsubmissions == 2 and can_edit %} @@ -818,6 +847,12 @@ {% endif %} {% endif %} + {% if not can_edit and progress.regtotal == 0 %} +

    + This project has been finalized. Now the project is blocked. +

    + {% endif %} + {% endif %} @@ -827,7 +862,6 @@ {% if assessment.ass_status == 1 or assessment.ass_status == 2 %}
    -
    {{ _("Duplicate") }} {{ _("Share") }} {{ _("Share") }} {{ _("Finalized") }}
    #} {#
    #} @@ -231,6 +243,22 @@
    + + + + + +

    Not allowed

    - {% set pbtype = "progress-bar-info" %} - {% if progress.regsubmissions == 0 %} -
    {{ _("Not yet started") }}
    - {% endif %} - {% if progress.regsubmissions == 1 %} -
    {{ _("On going") }}
    - {% endif %} - {% if progress.regsubmissions == 2 %} -
    {{ _("Closed") }}
    - {% if progress.regperc >= 100 %} - {% set pbtype = "progress-bar-success" %} - {% else %} - {% set pbtype = "progress-bar-warning" %} + {% if can_edit %} + {% set pbtype = "progress-bar-info" %} + {% if progress.regsubmissions == 0 %} +
    {{ _("Not yet started") }}
    + {% endif %} + {% if progress.regsubmissions == 1 %} +
    {{ _("On going") }}
    {% endif %} + {% if progress.regsubmissions == 2 %} +
    {{ _("Closed") }}
    + {% if progress.regperc >= 100 %} + {% set pbtype = "progress-bar-success" %} + {% else %} + {% set pbtype = "progress-bar-warning" %} + {% endif %} + {% endif %} +
    +
    +
    + {% else %} +
    {{ _("Project Finalized") }}
    {% endif %} -
    -
    -
    @@ -668,8 +689,7 @@ -
    +
    {% if progress.regsubmissions == 0 and can_edit %}
    @@ -677,7 +697,9 @@
    {% else %} @@ -688,8 +710,7 @@
    {% endif %} - - {% if progress.regsubmissions == 1 or progress.regsubmissions == 2 %} + {% if ((progress.regsubmissions == 1 or progress.regsubmissions == 2 ) and can_edit ) or (progress.regtotal > 0) %}

    {{ _("Statistics") }}

    @@ -703,10 +724,10 @@
    {{ _("Number of submissions") }}: {{ progress.regtotal }} - {% if progress.regtotal > 0 %}( - {{ ((progress.regtotal * 100)/activeProject.project_numobs)|round(1, 'floor') }} - %){% endif %}
    0 and progress.regsubmissions == 1 %}style="color: #ed5565;" {% endif %}>{{ _("Submissions with conflicts") }}:
    @@ -838,26 +872,31 @@ - + {{ assessment.assperc|round(1, 'floor') }} %) + {% endif %} + @@ -943,7 +982,7 @@ {% if assessment.ass_status == 1 %} {% endif %} {% endif %} - {% if assessment.errors > 0 and assessment.ass_status == 1 and can_edit %} + {% if assessment.errors > 0 and assessment.ass_status == 1 %} 5 or (activeProject.project_registration_and_analysis == 1 and progress.regtotal >=5) %} - {% if activeProject["access_type"] not in [4] %} -
    -
    -
    {{ _("Analysis") }}
    -
    -
    +
    +
    +
    {{ _("Analysis") }}
    +
    +
    + {% if total_ass_records > 5 or (activeProject.project_registration_and_analysis == 1 and progress.regtotal >=5)%} + {% if activeProject["access_type"] not in [4] %} {% if progress.metadata %} - {{ _("Start analysis") }} - + {{ _("Start analysis") }} {% else %} -
    - {{ _("To do the data analysis you must complete the section: ") }}

    - {{ _("Trial documentation") }} - -
    + {% if can_edit %} +
    + {{ _("To do the data analysis you must complete the section: ") }}

    + {{ _("Trial documentation") }} +
    + {% else %} + {{ _("Project finalized without the requirements to do the analysis") }} + {% endif %} {% endif %} -
    -
    -
    - {% endif %} - {% endif %} + {% else %} + {{ _("Your access type do not allow to generate the analysis") }} + {% endif %} + {% else %} + {% if can_edit %} + {{ _("The project did not reach the requirements to do the analysis") }} + {% else %} + {{ _("Project finalized without the requirements to do the analysis") }} + {% endif %} + {% endif %} +
    +
    +
    + {% endif %} - {% if hasActiveProject and can_edit and activeProject["access_type"] in [1] and activeUser.login == activeProject.owner.user_name %} +
    - {% endif %} {% block communityLink %} diff --git a/climmob/templates/email/close_project.jinja2 b/climmob/templates/email/close_project.jinja2 index 21741107..a217e9cf 100644 --- a/climmob/templates/email/close_project.jinja2 +++ b/climmob/templates/email/close_project.jinja2 @@ -79,12 +79,14 @@ {{ _("Finalized") }} {% endif %}
    -

    {{ _('Can you curate the project at this moment? Please click here') }}

    +

    {{ _('Would you like to curate the project at this moment?') }} {{ _('Please click here') }}

    {{ _('Thank you for collaborating in the 1000FARMS Network monitoring!') }}

    {% endblock email_message %} {% block email_goodbye %}

    {{ _('Best regards,') }}
    {{ _('The ClimMob team') }}

    +
    + {% endblock email_goodbye %} {% endblock email_body %}
    diff --git a/climmob/templates/email/close_project_participants_registration.jinja2 b/climmob/templates/email/close_project_participants_registration.jinja2 new file mode 100644 index 00000000..7b39f4b2 --- /dev/null +++ b/climmob/templates/email/close_project_participants_registration.jinja2 @@ -0,0 +1,84 @@ + + + + + + + + + + diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 799117e1..a7ea383d 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -14,7 +14,16 @@ {% endblock css %} @@ -49,25 +58,15 @@ {% endset %} {% if can_edit %} -
    +
    -
    {{ _('Confirmation details') }}
    {{ _('Warning') }} +
    {{ _('Confirmation details') }}
    + {{ _('Warning') }}
    -

    - {{ _('By finalizing this project, you confirm that it has reached completion. This action will permanently lock all data entries, profile settings, and metadata, so no further edits will be allowed. Please review the project details carefully before proceeding.') }} -

    - {% set total_ass_records = 0 %} - {% for assessment in progress["assessments"] %} - {% if assessment["ass_status"] == 1 or assessment["ass_status"] == 2 %} - {% set total_ass_records = total_ass_records + assessment["asstotal"] %} - {% endif %} - {% endfor %} -
    -
    +
    {% set pbtype = "progress-bar-info" %} - {% if progress.asssubmissions == 0 %} -
    {{ _("Not yet started") }}
    - {% endif %} - {% if assessment.ass_status == 1 %} -
    {{ _("On going") }}
    - {% endif %} - {% if assessment.ass_status == 2 %} -
    {{ _("Closed") }}
    - {% if assessment.assperc >= 100 %} - {% set pbtype = "progress-bar-success" %} - {% else %} - {% set pbtype = "progress-bar-warning" %} + {% if can_edit %} + {% if progress.asssubmissions == 0 %} +
    {{ _("Not yet started") }}
    + {% endif %} + {% if assessment.ass_status == 1 %} +
    {{ _("On going") }}
    {% endif %} + {% if assessment.ass_status == 2 %} +
    {{ _("Closed") }}
    + {% if assessment.assperc >= 100 %} + {% set pbtype = "progress-bar-success" %} + {% else %} + {% set pbtype = "progress-bar-warning" %} + {% endif %} + {% endif %} + {% else %} +
    {{ _("Project Finalized") }}
    {% endif %}
    + class="progress-bar {{ pbtype }}"> +
    @@ -887,9 +926,9 @@ {{ _("Number of submissions") }}: {{ assessment.asstotal }} {% if assessment.asstotal > 0 %}( - {{ assessment.assperc|round(1, 'floor') }} - %){% endif %}
    0 and assessment.ass_status == 1 %}style="color: #ed5565;" {% endif %}>{{ _("Submissions with conflicts") }}:
    @@ -126,22 +125,19 @@

    {{ _('If you are sure about it please, press the button.') }}

    - - {{ _('Confirm finalization of this project') }} + {{ _('Confirm project finalization') }}
    - - @@ -152,8 +148,8 @@
    -
    {{ _('Thanks for confirm the finish of your project') }}
    {{ _('Success') }} +
    {{ _('Thanks for confirm the finish of your project') }}
    + {{ _('Success') }}

    @@ -163,11 +159,13 @@

    {% endif %} - +{{ total_ass_records }} +{{ project_info.project_registration_and_analysis }} +{{ progress.regtotal }} - {% endblock pagecontent %} - {% block script %} {% endblock script %} \ No newline at end of file From 4eb70b7a9b6a97d2d804dbe9000d1521a426afc8 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Fri, 20 Mar 2026 13:11:32 -0600 Subject: [PATCH 25/29] final changes figma v11 --- climmob/templates/dashboard/dashboard.jinja2 | 16 +- .../templates/project/finishproject.jinja2 | 266 +++++++++++------- 2 files changed, 171 insertions(+), 111 deletions(-) diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index 1e533e78..5c7c5a12 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -102,9 +102,6 @@ {% endblock mainavheader %} {% block mainavitems %} -{#
  • #} -{# {{ _('New project') }}#} -{#
  • #} {% if hasActiveProject %}
  • {{ _("Completed") }} -{# {% endif %}#} {% elif can_edit %} {% if progress.technology and not progress.techalias %} {{ _("5. Prepare trial data collection") }} - {% if progress.assessments|length > 0 %} - 0 %} + {{ progress.assessments|length }} {{ _("forms") }} {% endif %} + title="{{ progress.assessments|length }} {{ _(" data collection moments created") }}">{{ progress.assessments|length }} {{ _("forms") }} + {% endif %} -
  • @@ -849,7 +845,7 @@ {% endif %} {% if not can_edit and progress.regtotal == 0 %}

    - This project has been finalized. Now the project is blocked. + {{ _("This project has been finalized. Now the project is blocked") }}

    {% endif %} @@ -1106,7 +1102,7 @@ {% else %}

    - {{ _('This project has been finalized. The project is now locked, and no further data entries, profile updates, or metadata changes are allowed.') }} + {{ _('This project has been finalized') }}.

    {% endif %} {% else %} diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 16bf68d1..131222a2 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -34,15 +34,12 @@ {% endblock topScripts %} {% block pagecontent %} - {% if error %} -
    {{ error }}
    - {% endif %} + {% set can_edit = True %} {% if activeProject.project_status == 3 %} {% set can_edit = False %} {% endif %} - {% set progress_status %} {% if project_info.project_regstatus == 0 %} {{ _("Registration not yet started") }} @@ -56,110 +53,177 @@ {% endif %} {% endif %} {% endset %} +{% set flag_assessments_errors = + progress.assessments | selectattr('errors', 'ne', 0) | list +%} + +{% set flag_registration_errors = progress.get('regerrors', 0) != 0 %} {% if can_edit %} -
    -
    -
    -
    {{ _('Confirmation details') }}
    - {{ _('Warning') }} -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + +
    {{ _('Field name') }}{{ _('Value') }}
    {{ _('Project name') }}{{ project_info["project_name"] }}
    {{ _('Creation date') }}{{ project_info["project_creationdate"] }}
    {{ _('Progress') }} - {% if project_info.project_regstatus == 0 %} - {{ _("Registration not yet started") }} - {% else %} - {% if project_info.project_regstatus == 1 %} - {{ _("Registration ongoing") }} - {% else %} - {% if project_info.project_regstatus == 2 %} - {% if project_info.project_assstatus == 0 %} - {{ _("Registration closed") }} +
    +
    +
    +
    +
    {{ _('Confirmation details') }}
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if project_info.project_registration_and_analysis == 0 %} + + + + {% endif %} - - - - - - - - -
    {{ _('Field name') }}{{ _('Status') }}
    {{ _('Project name') }}{{ project_info["project_name"] }}
    {{ _('Project Status') }} + {% if project_info["project_status"] == 0 %} + {{ _("Undefined") }} + {% elif project_info["project_status"] == 1 %} + {{ _("Definition") }} + {% elif project_info["project_status"] == 2 %} + {{ _("In progress") }} + {% elif project_info["project_status"] == 3 %} + {{ _("Finalized") }} + {% endif %} +
    {{ _('Project progress summary') }}

    {{ _("1. Project profile") }}

    + {{ _("Completed") }} +

    {{ _("2. Assign field agents") }}

    + {% if progress.enumerators %} + {{ _("Completed") }} + {% else %} + {{ _("Incomplete") }} + {% endif %} +

    {{ _("3. Assign technologies") }}

    + {% if progress.technology and progress.techalias %} + {{ _("Completed") }} + {% else %} + {{ _("Incomplete") }} + {% endif %} +

    + {% if project_info.project_registration_and_analysis == 0 %} + {{ _("4. Prepare participant registration form") }} + {% else %} + {{ _("4. Participant registration and trial data collection") }} + {% endif %} +

    + {% if progress.registry %} + {{ _("Completed") }} + {% else %} + {{ _("Incomplete") }} + {% endif %} +

    {{ _("5. Prepare trial data collection") }}

    + {% if progress.assessment %} + {{ _("Completed") }} {% else %} - {{ _("Trial data collection / Analysis ongoing") }} + {{ _("Incomplete") }} + {% endif %} - {% endif %} - {% endif %} +
    {{ _('Project Status') }} - {% if project_info["project_status"] == 0 %} - {{ _("Undefined") }} - {% elif project_info["project_status"] == 1 %} - {{ _("Definition") }} - {% elif project_info["project_status"] == 2 %} - {{ _("In progress") }} - {% elif project_info["project_status"] == 3 %} - {{ _("Finalized") }} - {% endif %} -
    +

    + {% if project_info.project_registration_and_analysis == 1 %} + {{ _("5. Trial documentation") }} + {% else %} + {{ _("6. Trial documentation") }} + {% endif %} +

    + {% if progress.metadata == True %} + {{ _("Completed") }}' + {% else %} + {{ _("Incomplete") }} + + {{ _('Warning') }} + {% endif %} +
    {{ _("Field data") }}

    {{ _("Data Collection") }}

    + {% if (total_ass_records > 5 or + (project_info.project_registration_and_analysis == 1 and progress.regtotal >=5)) %} + {{ _("Completed") }} + {% else %} + {{ _("Incomplete") }} + {% endif %} + {% if flag_assessments_errors or flag_registration_errors %} + {{ _('Warning') }} + {% endif %} +

    {{ _("Analysis") }}

    + {% if (total_ass_records > 5 or + (project_info.project_registration_and_analysis == 1 and progress.regtotal >=5)) + and progress.metadata == True %} + {{ _("Allowed") }} + {% else %} + {{ _('Not Allowed') }} + {% endif %} +
    +
    + + {{ _("Go back to project overview") }} +
    + + + {{ _('Confirm project finalization') }} + +
    + +
    -

    - {{ _('If you are sure about it please, press the button.') }} -

    - - - {{ _('Confirm project finalization') }} - -
    - -
    -
    -
    -
    - {% endif %} - - {% if not can_edit %} -
    -
    -
    -
    {{ _('Thanks for confirm the finish of your project') }}
    - {{ _('Success') }} -
    -
    -

    - {{ _('Thanks for confirm the finish of your project. We really appreciate your effort.') }} -

    From 28c313a0c1f1590c74ece7eab71df9129a564257 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Thu, 26 Mar 2026 08:54:21 -0600 Subject: [PATCH 26/29] final changes figma v12 --- .../templates/project/finishproject.jinja2 | 100 ++++++++---------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index 131222a2..df81c71a 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -61,50 +61,43 @@ {% if can_edit %}
    -
    +
    {{ _('Confirmation details') }}
    -
    -
    - +
    +
    + +

    {{ _('Project name') }}: {{ project_info["project_name"] }}

    +

    {{ _('Project status') }}: + {% if project_info["project_status"] == 0 %} + {{ _("Undefined") }} + {% elif project_info["project_status"] == 1 %} + {{ _("Definition") }} + {% elif project_info["project_status"] == 2 %} + {{ _("In progress") }} + {% elif project_info["project_status"] == 3 %} + {{ _("Finalized") }} + {% endif %} +

    +
    + +
    + - - - - + + + - - - - - - - - - - - - - + - + - +
    {{ _('Field name') }}{{ _('Status') }}
    {{ _('Project progress') }}
    {{ _('Project name') }}{{ project_info["project_name"] }}
    {{ _('Project Status') }} - {% if project_info["project_status"] == 0 %} - {{ _("Undefined") }} - {% elif project_info["project_status"] == 1 %} - {{ _("Definition") }} - {% elif project_info["project_status"] == 2 %} - {{ _("In progress") }} - {% elif project_info["project_status"] == 3 %} - {{ _("Finalized") }} - {% endif %} -
    {{ _('Project progress summary') }}

    {{ _("1. Project profile") }}

    {{ _("Completed") }}

    {{ _("2. Assign field agents") }}

    @@ -172,7 +165,7 @@ {% endif %}
    {{ _("Field data") }}
    {{ _("Fiel data status") }}

    {{ _("Data Collection") }}

    @@ -199,30 +192,31 @@ {% endif %}
    + + + {{ _("Go back to project overview") }} +
    + + + {{ _('Confirm project finalization') }} + +
    + +
    - - {{ _("Go back to project overview") }} -
    - - - {{ _('Confirm project finalization') }} - -
    - -
    From c21fa2252ef7ebb50637aded147ae7c9d9a90e17 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Thu, 26 Mar 2026 14:41:39 -0600 Subject: [PATCH 27/29] final changes figma v13 --- climmob/templates/dashboard/dashboard.jinja2 | 2 +- ...e_project_participants_registration.jinja2 | 9 +-- .../templates/project/finishproject.jinja2 | 2 +- climmob/templates/project/projectList.jinja2 | 57 +++---------------- 4 files changed, 13 insertions(+), 57 deletions(-) diff --git a/climmob/templates/dashboard/dashboard.jinja2 b/climmob/templates/dashboard/dashboard.jinja2 index 5c7c5a12..d3b66369 100755 --- a/climmob/templates/dashboard/dashboard.jinja2 +++ b/climmob/templates/dashboard/dashboard.jinja2 @@ -1063,7 +1063,7 @@ {% endif %} {% else %} {% if can_edit %} - {{ _("The project did not reach the requirements to do the analysis") }} + {{ _("The project does not meet the requirements to run the analysis.") }} {% else %} {{ _("Project finalized without the requirements to do the analysis") }} {% endif %} diff --git a/climmob/templates/email/close_project_participants_registration.jinja2 b/climmob/templates/email/close_project_participants_registration.jinja2 index 7b39f4b2..7bb514b2 100644 --- a/climmob/templates/email/close_project_participants_registration.jinja2 +++ b/climmob/templates/email/close_project_participants_registration.jinja2 @@ -36,16 +36,13 @@

    {{ _('We would like to inform you that the project ') }} {{ project_info["project_cod"] }} - {{ project_info["project_name"] }} - {{ _('has been officially finalized ') }}. + {{ _('has been officially finalized') }}.

    {{ _('As part of the project lifecycle in ClimMob, once it is finalized, all related activities are - closed and the project becomes locked for further modifications') }}. + closed and the project becomes locked for further modifications') }}. {{ _("This means that no additional data entries, edits, or updates can be made") }}.

    -

    - {{ _("This means that no additional data entries, edits, or updates can be made") }}. -

    -

    {{ _('You may still access the project to review its information and results according to the access +

    {{ _('You may still access the project to review its information and results according to the access permissions previously granted by the project owner') }}.

    {{ _("If applicable, and provided the project meets the minimum requirements, you will also be able to diff --git a/climmob/templates/project/finishproject.jinja2 b/climmob/templates/project/finishproject.jinja2 index df81c71a..a4a4f331 100644 --- a/climmob/templates/project/finishproject.jinja2 +++ b/climmob/templates/project/finishproject.jinja2 @@ -200,7 +200,7 @@
    {% if userProjects %} -{#

    #} -{# #} @@ -131,8 +125,6 @@ {% endif %} - - @@ -149,16 +141,6 @@ {% endif %} -{# #} - {% if is_owner %} - {% if can_edit %} - - - {% else %} - - {% endif %} - {% else %} - - {% endif %} {% endfor %} @@ -316,7 +276,6 @@ columnDefs: [ {target: -1, columnControl: [], orderable: false }, {target: -2, columnControl: [], orderable: false }, - {target: -3, columnControl: [], orderable: false }, { targets: hiddenIndexes, visible: false, From 94a3cf9085ce45822e4bf626313313ecc4a6a043 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Thu, 26 Mar 2026 15:00:44 -0600 Subject: [PATCH 28/29] final changes figma v13. delete can edit on proyectlist --- climmob/templates/project/projectList.jinja2 | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/climmob/templates/project/projectList.jinja2 b/climmob/templates/project/projectList.jinja2 index 8e8d03a6..9249abaa 100644 --- a/climmob/templates/project/projectList.jinja2 +++ b/climmob/templates/project/projectList.jinja2 @@ -130,16 +130,6 @@ {% for project in userProjects %} - {% set can_edit = true %} - {% if project.project_status == 3 %} - {% set can_edit = false %} - {% endif %} - - {% set is_owner = false %} - {% if activeUser.login == project.owner.user_name %} - {% set is_owner = True %} - {% endif %} -
    {{ _("Project overview") }} {{ _("Project ID") }} {{ _("Project name") }} {{ _("Owner") }} {{ _("Duplicate") }} {{ _("Share") }} {{ _("Finalized") }}
    #} -{#
    #} -{# #} -{#
    #} -{# {% if project.project_dashboard == 1%}#} -{# #} -{# {% endif %}#} -{#

    {{ project.project_cod }}

    @@ -171,7 +153,6 @@
    -{#
    #}

    {% if project.access_type== 1 %} {{ _("Owner") }} @@ -195,26 +176,21 @@

    {{ project.project_piemail }}

    -

    - {% if project.project_regstatus == 0 %} - {{ _("Design") }} + {% if not can_edit %} + {{ _("Finalized") }} {% else %} - {% if project.project_regstatus == 1 %} - {{ _("Registration") }} + {% if project.project_regstatus == 0 %} + {{ _("Design") }} {% else %} - {% if project.project_regstatus == 2 %} - - {% if project.project_assstatus == 0 %} - {{ _("Data collection") }} - {% else %} + {% if project.project_regstatus == 1 %} + {{ _("Registration") }} + {% else %} + {% if project.project_regstatus == 2 %} {{ _("Data collection") }} {% endif %} - {% endif %} - {% endif %} - {% endif %}

    @@ -243,22 +219,6 @@
    - - - - - -

    Not allowed

    @@ -177,7 +167,7 @@

    - {% if not can_edit %} + {% if project.project_status == 3 %} {{ _("Finalized") }} {% else %} {% if project.project_regstatus == 0 %} From 59f665c0fbc6505dd4679a90854e13ed60b678a1 Mon Sep 17 00:00:00 2001 From: PabloMrBot Date: Tue, 14 Apr 2026 12:07:35 -0600 Subject: [PATCH 29/29] changes on the way of country is display/ changes on the recipents to get the real data / fix the test according to the changes / fix a bug on the projectshare --- climmob/processes/db/project.py | 5 ++ ...e_project_participants_registration.jinja2 | 4 +- .../templates/project/modifyproject.jinja2 | 2 - .../tests/test_utils/test_views_project.py | 49 +++++++++++++------ climmob/views/Share/projectShare.py | 2 + climmob/views/project.py | 21 ++++---- 6 files changed, 53 insertions(+), 30 deletions(-) diff --git a/climmob/processes/db/project.py b/climmob/processes/db/project.py index 1f35303d..7681986c 100644 --- a/climmob/processes/db/project.py +++ b/climmob/processes/db/project.py @@ -457,6 +457,11 @@ def extraDetailsForProject(activeProject, request): activeProject["languages"] = getPrjLangInProject( activeProject["project_id"], request ) + activeProject["Country"] = ( + request.dbsession.query(Country) + .filter_by(cnty_cod=activeProject["project_cnty"]) + .first() + ) for plugin in p.PluginImplementations(p.IProjectTechnologyOptions): activeProject = plugin.get_extra_information_for_data_exchange( diff --git a/climmob/templates/email/close_project_participants_registration.jinja2 b/climmob/templates/email/close_project_participants_registration.jinja2 index 7bb514b2..939e1185 100644 --- a/climmob/templates/email/close_project_participants_registration.jinja2 +++ b/climmob/templates/email/close_project_participants_registration.jinja2 @@ -52,7 +52,7 @@

      {{ _('Project name') }}: {{ project_info["project_name"] }}
      {{ _('Project principal investigator') }}: {{ project_info["project_pi"] }}
    -
      {{ _('Country') }}: {{ project_info["project_cnty"] }}
    +
      {{ _('Country') }}: {{ project_info["Country"].cnty_name }}
      {{ _('Status') }}: {% if project_info["project_status"] == 0 %} {{ _("Undefined") }} @@ -65,7 +65,7 @@ {% endif %}

    {{ _("If you have questions regarding the project or its outcomes, please contact the project owner or the principal investigator.") }}

    -

    {{ _('Thank you for your participation and collaboration.') }}.

    +

    {{ _('Thank you for your participation and collaboration') }}.

    {% endblock email_message %} {% block email_goodbye %} diff --git a/climmob/templates/project/modifyproject.jinja2 b/climmob/templates/project/modifyproject.jinja2 index 9e1ae71a..1676a2af 100644 --- a/climmob/templates/project/modifyproject.jinja2 +++ b/climmob/templates/project/modifyproject.jinja2 @@ -65,7 +65,6 @@ {% set showNote=True %} {% include 'snippets/project/project_form.jinja2' %} - {% if not finished %} {% if permissionForChanges %}
    @@ -84,7 +83,6 @@

    {% endif %} - {% endif %} {% endblock newprjform %}
    diff --git a/climmob/tests/test_utils/test_views_project.py b/climmob/tests/test_utils/test_views_project.py index 4d87b93d..8e688019 100644 --- a/climmob/tests/test_utils/test_views_project.py +++ b/climmob/tests/test_utils/test_views_project.py @@ -1618,10 +1618,6 @@ def setUp(self): self.view.request.project = "PRJ001" self.view.request.registry.settings = {"email.from": "email_send@test.com"} - self.view.project_info = { - "project_cod": MagicMock(name="fake_code"), - "project_id": MagicMock(name="fake_id"), - } fake_now = datetime(2024, 1, 1, 12, 0, 0).strftime("%Y-%m-%d %H:%M:%S") self.view.request.translate = lambda text: text @@ -1660,6 +1656,16 @@ def setUp(self): self.mock_email_sender.return_value.send_email.return_value = None self.mock_log = self.log_patcher.start() + self.fake_project_id = MagicMock(name="fake_id") + self.fake_project_cod = MagicMock(name="fake_cod") + self.mock_project_info.return_value = { + "project_id": self.fake_project_id, + "project_cod": self.fake_project_cod, + } + self.mock_get_project_id.return_value = { + "project_id": self.fake_project_id, + "project_cod": self.fake_project_cod, + } self.mock_success.return_value = (True, "") self.mock_admin_users_patcher.return_value = [ @@ -1675,7 +1681,6 @@ def setUp(self): }, True self.view.request.matchdict = {"project": "PRJ001"} - self.mock_get_project_id.return_value = (MagicMock(name="fake_id"),) self.addCleanup(self.get_project_id_patcher.stop) self.addCleanup(self.set_active_project_patcher.stop) @@ -1717,7 +1722,7 @@ def tearDown(self): "email/close_project.jinja2", { "date": self.mock_datetime.now.return_value.strftime.return_value, - "project_info": self.view.project_info, + "project_info": self.mock_project_info.return_value, "_": self.view.request.translate, "link": self.view.request.route_url("projectsSummaryRecent"), "logo": self.view.request.url_for_static("landing/climmob2.png"), @@ -1727,7 +1732,7 @@ def tearDown(self): self.mock_msg.assert_called_once_with( self.mock_text.return_value, "✅ Project " - + str(self.view.project_info["project_cod"]) + + str(self.mock_get_project_id.return_value["project_cod"]) + " has been finalized", [("name1", "email1"), ("name2", "email2")], self.view.request.registry.settings["email.from"], @@ -1735,8 +1740,8 @@ def tearDown(self): if self.mock_progress.called: self.mock_progress.assert_called_once_with( self.view.user.login, - self.mock_project_info.return_value["project_cod"], - self.mock_project_info.return_value["project_id"], + self.view.request.project, + self.mock_get_project_id.return_value, self.view.request, ) super().tearDown() @@ -1746,7 +1751,7 @@ def test_finish_project_view_get(self): self.assertEqual( response, { - "project_info": self.mock_project_info.return_value, + "project_info": self.mock_get_project_id.return_value, "progress": { "data_progress": "result_data", "assessments": [{"ass_status": 1, "asstotal": 1}], @@ -1777,32 +1782,44 @@ def test_finish_project_view_post_fail(self): ) def test_send_email_success(self): - response = self.view.send_email_notification(self.view.project_info) + response = self.view.send_email_notification( + self.mock_project_info.return_value + ) self.assertEqual(response, True) def test_send_email_no_mail_from(self): self.view.request.registry.settings = {} - response = self.view.send_email_notification(self.view.project_info) + response = self.view.send_email_notification( + self.mock_project_info.return_value + ) self.assertEqual(response, False) def test_send_email_no_admin(self): self.mock_admin_users_patcher.return_value = [] - response = self.view.send_email_notification(self.view.project_info) + response = self.view.send_email_notification( + self.mock_project_info.return_value + ) self.assertEqual(response, False) def test_send_email_template_error(self): self.mock_text.side_effect = Exception("template error") - response = self.view.send_email_notification(self.view.project_info) + response = self.view.send_email_notification( + self.mock_project_info.return_value + ) self.assertEqual(response, False) def test_send_email_msg_error(self): self.mock_msg.side_effect = Exception("msg error") - response = self.view.send_email_notification(self.view.project_info) + response = self.view.send_email_notification( + self.mock_project_info.return_value + ) self.assertEqual(response, False) def test_send_email_error(self): self.mock_email_sender.side_effect = Exception("server error") - response = self.view.send_email_notification(self.view.project_info) + response = self.view.send_email_notification( + self.mock_project_info.return_value + ) self.assertEqual(response, False) diff --git a/climmob/views/Share/projectShare.py b/climmob/views/Share/projectShare.py index 9bc464a1..a27cfd65 100644 --- a/climmob/views/Share/projectShare.py +++ b/climmob/views/Share/projectShare.py @@ -14,6 +14,7 @@ get_collaborators_in_project, remove_collaborator, getAccessTypeForProject, + setActiveProject, ) from climmob.views.classes import privateView from climmob.views.validators.ProjectExistsValidator import ProjectExistsValidator @@ -32,6 +33,7 @@ def processView(self): activeProjectId = getTheProjectIdForOwner( activeProjectUser, activeProjectCod, self.request ) + setActiveProject(self.user.login, activeProjectId, self.request) accessType = getAccessTypeForProject( self.user.login, activeProjectId, self.request diff --git a/climmob/views/project.py b/climmob/views/project.py index 00f80335..0a8eff4b 100644 --- a/climmob/views/project.py +++ b/climmob/views/project.py @@ -984,8 +984,8 @@ def get(self): project_info = getActiveProject(self.user.login, self.request) progress, pcompleted = getProjectProgress( request_activeUSer, - project_info["project_cod"], - project_info["project_id"], + request_activeProjectCod, + activeProjectId, self.request, ) total_ass_records = 0 @@ -997,7 +997,6 @@ def get(self): "project_info": project_info, "progress": progress, "total_ass_records": total_ass_records, - } def post(self): @@ -1082,24 +1081,26 @@ def send_collaborators_email_notification(self, project_info): "ClimMob has no email settings in place. Email service is disabled." ) return False - related_collaborators = get_collaborators_in_project(self.request, project_info["project_id"] ) - recipients = [("Pablo O.", "porozco@mrbotcr.com")] + related_collaborators = get_collaborators_in_project( + self.request, project_info["project_id"] + ) + recipients = [] for collaborator in related_collaborators: - recipients.append((collaborator["user_fullname"], collaborator["user_email"])) + recipients.append( + (collaborator["user_fullname"], collaborator["user_email"]) + ) if not recipients: log.warning("Email didn't send. No recipients found.") return False - subject = ( - "Project " + str(project_info["project_cod"]) + " has been finalized" - ) + subject = "Project " + str(project_info["project_cod"]) + " has been finalized" try: text = render_template( "email/close_project_participants_registration.jinja2", { "project_info": project_info, "_": _, - "logo": self.request.url_for_static('landing/climmob2.png') + "logo": self.request.url_for_static("landing/climmob2.png"), }, ) except Exception as e: