Browse Source

Merge pull request #13 from tracim/fix/742_backend_should_generate_index.html

inkhey 6 years ago
parent
commit
bd9bae009d
No account linked to committer's email

+ 1 - 0
.gitignore View File

5
 frontend_app_html-document/dist/html-document.app.js
5
 frontend_app_html-document/dist/html-document.app.js
6
 frontend_lib/dist/tracim_frontend_lib.js
6
 frontend_lib/dist/tracim_frontend_lib.js
7
 npm-debug.log
7
 npm-debug.log
8
+package-lock.json

+ 7 - 0
backend/development.ini.sample View File

186
 ## return error
186
 ## return error
187
 # preview.jpg.restricted_dims = True
187
 # preview.jpg.restricted_dims = True
188
 
188
 
189
+### Frontend
190
+frontend.serve = True
191
+# You can set dist folder of tracim frontend. by default, system
192
+# will try to get it automatically according to tracim_v2 repository
193
+# organisation.
194
+# frontend.dist_folder_path = /home/user/tracim_v2/frontend/dist
195
+
189
 ###
196
 ###
190
 # wsgi server configuration
197
 # wsgi server configuration
191
 ###
198
 ###

+ 3 - 0
backend/setup.py View File

41
     'lxml',
41
     'lxml',
42
     'redis',
42
     'redis',
43
     'rq',
43
     'rq',
44
+    # frontend file serve
45
+    'pyramid_mako',
46
+    'spectra',
44
 ]
47
 ]
45
 
48
 
46
 tests_require = [
49
 tests_require = [

+ 9 - 3
backend/tests_configs.ini View File

4
 depot_storage_dir = /tmp/test/depot
4
 depot_storage_dir = /tmp/test/depot
5
 user.auth_token.validity = 604800
5
 user.auth_token.validity = 604800
6
 preview_cache_dir = /tmp/test/preview_cache
6
 preview_cache_dir = /tmp/test/preview_cache
7
-
7
+website.base_url = http://localhost:6543
8
 [app:command_test]
8
 [app:command_test]
9
 use = egg:tracim_backend
9
 use = egg:tracim_backend
10
 sqlalchemy.url = sqlite:///tracim_test.sqlite
10
 sqlalchemy.url = sqlite:///tracim_test.sqlite
12
 depot_storage_dir = /tmp/test/depot
12
 depot_storage_dir = /tmp/test/depot
13
 user.auth_token.validity = 604800
13
 user.auth_token.validity = 604800
14
 preview_cache_dir = /tmp/test/preview_cache
14
 preview_cache_dir = /tmp/test/preview_cache
15
+website.base_url = http://localhost:6543
15
 
16
 
16
 [mail_test]
17
 [mail_test]
17
 sqlalchemy.url = sqlite:///:memory:
18
 sqlalchemy.url = sqlite:///:memory:
37
 email.notification.smtp.port = 1025
38
 email.notification.smtp.port = 1025
38
 email.notification.smtp.user = test_user
39
 email.notification.smtp.user = test_user
39
 email.notification.smtp.password = just_a_password
40
 email.notification.smtp.password = just_a_password
41
+website.base_url = http://localhost:6543
40
 
42
 
41
 [mail_test_async]
43
 [mail_test_async]
42
 sqlalchemy.url = sqlite:///:memory:
44
 sqlalchemy.url = sqlite:///:memory:
63
 email.notification.smtp.port = 1025
65
 email.notification.smtp.port = 1025
64
 email.notification.smtp.user = test_user
66
 email.notification.smtp.user = test_user
65
 email.notification.smtp.password = just_a_password
67
 email.notification.smtp.password = just_a_password
68
+website.base_url = http://localhost:6543
66
 
69
 
67
 [functional_test]
70
 [functional_test]
68
 sqlalchemy.url = sqlite:///tracim_test.sqlite
71
 sqlalchemy.url = sqlite:///tracim_test.sqlite
72
 preview_cache_dir = /tmp/test/preview_cache
75
 preview_cache_dir = /tmp/test/preview_cache
73
 preview.jpg.restricted_dims = True
76
 preview.jpg.restricted_dims = True
74
 email.notification.activated = false
77
 email.notification.activated = false
78
+website.base_url = http://localhost:6543
75
 
79
 
76
 [functional_test_no_db]
80
 [functional_test_no_db]
77
 sqlalchemy.url = sqlite://
81
 sqlalchemy.url = sqlite://
81
 preview_cache_dir = /tmp/test/preview_cache
85
 preview_cache_dir = /tmp/test/preview_cache
82
 preview.jpg.restricted_dims = True
86
 preview.jpg.restricted_dims = True
83
 email.notification.activated = false
87
 email.notification.activated = false
88
+website.base_url = http://localhost:6543
84
 
89
 
85
 [functional_test_with_mail_test_sync]
90
 [functional_test_with_mail_test_sync]
86
 sqlalchemy.url = sqlite:///tracim_test.sqlite
91
 sqlalchemy.url = sqlite:///tracim_test.sqlite
105
 email.notification.smtp.port = 1025
110
 email.notification.smtp.port = 1025
106
 email.notification.smtp.user = test_user
111
 email.notification.smtp.user = test_user
107
 email.notification.smtp.password = just_a_password
112
 email.notification.smtp.password = just_a_password
108
-
113
+website.base_url = http://localhost:6543
109
 
114
 
110
 [functional_test_with_mail_test_async]
115
 [functional_test_with_mail_test_async]
111
 sqlalchemy.url = sqlite:///tracim_test.sqlite
116
 sqlalchemy.url = sqlite:///tracim_test.sqlite
129
 email.notification.smtp.server = 127.0.0.1
134
 email.notification.smtp.server = 127.0.0.1
130
 email.notification.smtp.port = 1025
135
 email.notification.smtp.port = 1025
131
 email.notification.smtp.user = test_user
136
 email.notification.smtp.user = test_user
132
-email.notification.smtp.password = just_a_password
137
+email.notification.smtp.password = just_a_password
138
+website.base_url = http://localhost:6543

+ 9 - 4
backend/tracim_backend/__init__.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-
3
-
4
 try:  # Python 3.5+
2
 try:  # Python 3.5+
5
     from http import HTTPStatus
3
     from http import HTTPStatus
6
 except ImportError:
4
 except ImportError:
9
 from pyramid.config import Configurator
7
 from pyramid.config import Configurator
10
 from pyramid.authentication import BasicAuthAuthenticationPolicy
8
 from pyramid.authentication import BasicAuthAuthenticationPolicy
11
 from hapic.ext.pyramid import PyramidContext
9
 from hapic.ext.pyramid import PyramidContext
12
-from pyramid.exceptions import NotFound
13
 from sqlalchemy.exc import OperationalError
10
 from sqlalchemy.exc import OperationalError
14
 
11
 
15
 from tracim_backend.extensions import hapic
12
 from tracim_backend.extensions import hapic
30
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
27
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
31
 from tracim_backend.views.contents_api.comment_controller import CommentController
28
 from tracim_backend.views.contents_api.comment_controller import CommentController
32
 from tracim_backend.views.contents_api.file_controller import FileController
29
 from tracim_backend.views.contents_api.file_controller import FileController
30
+from tracim_backend.views.frontend import FrontendController
33
 from tracim_backend.views.errors import ErrorSchema
31
 from tracim_backend.views.errors import ErrorSchema
34
 from tracim_backend.exceptions import NotAuthenticated
32
 from tracim_backend.exceptions import NotAuthenticated
33
+from tracim_backend.exceptions import PageNotFound
35
 from tracim_backend.exceptions import UserNotActive
34
 from tracim_backend.exceptions import UserNotActive
36
 from tracim_backend.exceptions import InvalidId
35
 from tracim_backend.exceptions import InvalidId
37
 from tracim_backend.exceptions import InsufficientUserProfile
36
 from tracim_backend.exceptions import InsufficientUserProfile
86
     hapic.set_context(context)
85
     hapic.set_context(context)
87
     # INFO - G.M - 2018-07-04 - global-context exceptions
86
     # INFO - G.M - 2018-07-04 - global-context exceptions
88
     # Not found
87
     # Not found
89
-    context.handle_exception(NotFound, HTTPStatus.NOT_FOUND)
88
+    context.handle_exception(PageNotFound, HTTPStatus.NOT_FOUND)
90
     # Bad request
89
     # Bad request
91
     context.handle_exception(WorkspaceNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
90
     context.handle_exception(WorkspaceNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
92
     context.handle_exception(UserNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
91
     context.handle_exception(UserNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
106
     context.handle_exception(OperationalError, HTTPStatus.INTERNAL_SERVER_ERROR)
105
     context.handle_exception(OperationalError, HTTPStatus.INTERNAL_SERVER_ERROR)
107
     context.handle_exception(Exception, HTTPStatus.INTERNAL_SERVER_ERROR)
106
     context.handle_exception(Exception, HTTPStatus.INTERNAL_SERVER_ERROR)
108
 
107
 
108
+
109
     # Add controllers
109
     # Add controllers
110
     session_controller = SessionController()
110
     session_controller = SessionController()
111
     system_controller = SystemController()
111
     system_controller = SystemController()
124
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
124
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
125
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
125
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
126
 
126
 
127
+    if app_config.FRONTEND_SERVE:
128
+        configurator.include('pyramid_mako')
129
+        frontend_controller = FrontendController(app_config.FRONTEND_DIST_FOLDER_PATH)  # nopep8
130
+        configurator.include(frontend_controller.bind)
131
+
127
     hapic.add_documentation_view(
132
     hapic.add_documentation_view(
128
         '/api/v2/doc',
133
         '/api/v2/doc',
129
         'Tracim v2 API',
134
         'Tracim v2 API',

+ 31 - 0
backend/tracim_backend/config.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from urllib.parse import urlparse
2
 from urllib.parse import urlparse
3
+
4
+import os
3
 from paste.deploy.converters import asbool
5
 from paste.deploy.converters import asbool
4
 from tracim_backend.lib.utils.logger import logger
6
 from tracim_backend.lib.utils.logger import logger
5
 from depot.manager import DepotManager
7
 from depot.manager import DepotManager
79
             'website.base_url',
81
             'website.base_url',
80
             '',
82
             '',
81
         )
83
         )
84
+        if not self.WEBSITE_BASE_URL:
85
+            raise Exception(
86
+                'website.base_url is needed in order to have correct path in'
87
+                'few place like in email.'
88
+                'You should set it with frontend root url.'
89
+            )
82
 
90
 
83
         # TODO - G.M - 26-03-2018 - [Cleanup] These params seems deprecated for tracimv2,  # nopep8
91
         # TODO - G.M - 26-03-2018 - [Cleanup] These params seems deprecated for tracimv2,  # nopep8
84
         # Verify this
92
         # Verify this
435
 
443
 
436
         self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims
444
         self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims
437
 
445
 
446
+        self.FRONTEND_SERVE = asbool(settings.get(
447
+            'frontend.serve', False
448
+        ))
449
+
450
+        # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder
451
+        # is probably in frontend subfolder
452
+        # of tracim_v2 parent of both backend and frontend
453
+        backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # nopep8
454
+        tracim_v2_folder = os.path.dirname(backend_folder)
455
+        frontend_dist_folder = os.path.join(tracim_v2_folder, 'frontend', 'dist')  # nopep8
456
+
457
+        self.FRONTEND_DIST_FOLDER_PATH = settings.get(
458
+            'frontend.dist_folder_path', frontend_dist_folder
459
+        )
460
+
461
+        # INFO - G.M - 2018-08-06 - We check dist folder existence
462
+        if self.FRONTEND_SERVE and not os.path.isdir(self.FRONTEND_DIST_FOLDER_PATH):  # nopep8
463
+            raise Exception(
464
+                'ERROR: {} folder does not exist as folder. '
465
+                'please set frontend.dist_folder.path'
466
+                'with a correct value'.format(self.FRONTEND_DIST_FOLDER_PATH)
467
+            )
468
+
438
     def configure_filedepot(self):
469
     def configure_filedepot(self):
439
         depot_storage_name = self.DEPOT_STORAGE_NAME
470
         depot_storage_name = self.DEPOT_STORAGE_NAME
440
         depot_storage_path = self.DEPOT_STORAGE_DIR
471
         depot_storage_path = self.DEPOT_STORAGE_DIR

+ 4 - 0
backend/tracim_backend/exceptions.py View File

207
 
207
 
208
 class TooShortAutocompleteString(TracimException):
208
 class TooShortAutocompleteString(TracimException):
209
     pass
209
     pass
210
+
211
+
212
+class PageNotFound(TracimException):
213
+    pass

+ 1 - 1
backend/tracim_backend/lib/core/notifications.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from sqlalchemy.orm import Session
2
 from sqlalchemy.orm import Session
3
 
3
 
4
-from tracim_backend import CFG
4
+from tracim_backend.config import CFG
5
 from tracim_backend.lib.utils.logger import logger
5
 from tracim_backend.lib.utils.logger import logger
6
 from tracim_backend.models.auth import User
6
 from tracim_backend.models.auth import User
7
 from tracim_backend.models.data import Content
7
 from tracim_backend.models.data import Content

+ 1 - 1
backend/tracim_backend/lib/core/user.py View File

9
 from sqlalchemy.orm import Query
9
 from sqlalchemy.orm import Query
10
 from sqlalchemy.orm.exc import NoResultFound
10
 from sqlalchemy.orm.exc import NoResultFound
11
 
11
 
12
-from tracim_backend import CFG
12
+from tracim_backend.config import CFG
13
 from tracim_backend.models.auth import User
13
 from tracim_backend.models.auth import User
14
 from tracim_backend.models.auth import Group
14
 from tracim_backend.models.auth import Group
15
 from tracim_backend.exceptions import NoUserSetted
15
 from tracim_backend.exceptions import NoUserSetted

+ 1 - 1
backend/tracim_backend/lib/core/userworkspace.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import typing
2
 import typing
3
 
3
 
4
-from tracim_backend import CFG
4
+from tracim_backend.config import CFG
5
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
5
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
6
 from tracim_backend.models.roles import WorkspaceRoles
6
 from tracim_backend.models.roles import WorkspaceRoles
7
 
7
 

+ 1 - 1
backend/tracim_backend/lib/core/workspace.py View File

5
 from sqlalchemy.orm import Session
5
 from sqlalchemy.orm import Session
6
 from sqlalchemy.orm.exc import NoResultFound
6
 from sqlalchemy.orm.exc import NoResultFound
7
 
7
 
8
-from tracim_backend import CFG
8
+from tracim_backend.config import CFG
9
 from tracim_backend.exceptions import EmptyLabelNotAllowed
9
 from tracim_backend.exceptions import EmptyLabelNotAllowed
10
 from tracim_backend.exceptions import WorkspaceNotFound
10
 from tracim_backend.exceptions import WorkspaceNotFound
11
 from tracim_backend.lib.utils.translation import fake_translator as _
11
 from tracim_backend.lib.utils.translation import fake_translator as _

+ 37 - 17
backend/tracim_backend/lib/mail_notifier/notifier.py View File

10
 from mako.template import Template
10
 from mako.template import Template
11
 from sqlalchemy.orm import Session
11
 from sqlalchemy.orm import Session
12
 
12
 
13
-from tracim_backend import CFG
13
+from tracim_backend.config import CFG
14
 from tracim_backend.lib.core.notifications import INotifier
14
 from tracim_backend.lib.core.notifications import INotifier
15
 from tracim_backend.lib.mail_notifier.sender import EmailSender
15
 from tracim_backend.lib.mail_notifier.sender import EmailSender
16
 from tracim_backend.lib.mail_notifier.utils import SmtpConfiguration, EST
16
 from tracim_backend.lib.mail_notifier.utils import SmtpConfiguration, EST
17
 from tracim_backend.lib.mail_notifier.sender import send_email_through
17
 from tracim_backend.lib.mail_notifier.sender import send_email_through
18
 from tracim_backend.lib.core.workspace import WorkspaceApi
18
 from tracim_backend.lib.core.workspace import WorkspaceApi
19
 from tracim_backend.lib.utils.logger import logger
19
 from tracim_backend.lib.utils.logger import logger
20
-from tracim_backend.models import User
20
+from tracim_backend.lib.utils.utils import get_login_frontend_url
21
+from tracim_backend.lib.utils.utils import get_email_logo_frontend_url
21
 from tracim_backend.models.auth import User
22
 from tracim_backend.models.auth import User
22
 from tracim_backend.models.contents import CONTENT_TYPES
23
 from tracim_backend.models.contents import CONTENT_TYPES
24
+from tracim_backend.models.context_models import ContentInContext
25
+from tracim_backend.models.context_models import WorkspaceInContext
23
 from tracim_backend.models.data import ActionDescription
26
 from tracim_backend.models.data import ActionDescription
24
 from tracim_backend.models.data import Content
27
 from tracim_backend.models.data import Content
25
 from tracim_backend.models.data import UserRoleInWorkspace
28
 from tracim_backend.models.data import UserRoleInWorkspace
234
             show_archived=True,
237
             show_archived=True,
235
             show_deleted=True,
238
             show_deleted=True,
236
         ).get_one(event_content_id, CONTENT_TYPES.Any_SLUG)
239
         ).get_one(event_content_id, CONTENT_TYPES.Any_SLUG)
237
-        main_content = content.parent if content.type == CONTENT_TYPES.Comment.slug else content
240
+        workspace_api = WorkspaceApi(
241
+            session=self.session,
242
+            current_user=user,
243
+            config=self.config,
244
+        )
245
+        workpace_in_context = workspace_api.get_workspace_with_context(workspace_api.get_one(content.workspace_id))  # nopep8
246
+        main_content = content.parent if content.type == CONTENT_TYPES.Comment.slug else content  # nopep8
238
         notifiable_roles = WorkspaceApi(
247
         notifiable_roles = WorkspaceApi(
239
             current_user=user,
248
             current_user=user,
240
             session=self.session,
249
             session=self.session,
265
             # INFO - G.M - 2017-11-15 - set content_id in header to permit reply
274
             # INFO - G.M - 2017-11-15 - set content_id in header to permit reply
266
             # references can have multiple values, but only one in this case.
275
             # references can have multiple values, but only one in this case.
267
             replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8
276
             replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8
268
-                '{content_id}',str(content.content_id)
277
+                '{content_id}', str(content.content_id)
269
             )
278
             )
270
 
279
 
271
             reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8
280
             reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8
297
             # To link this email to a content we create a virtual parent
306
             # To link this email to a content we create a virtual parent
298
             # in reference who contain the content_id.
307
             # in reference who contain the content_id.
299
             message['References'] = formataddr(('',reference_addr))
308
             message['References'] = formataddr(('',reference_addr))
300
-            body_text = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
301
-            body_html = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
309
+            content_in_context = content_api.get_content_in_context(content)
310
+            body_text = self._build_email_body_for_content(
311
+                self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT,
312
+                role,
313
+                content_in_context,
314
+                workpace_in_context,
315
+                user
316
+            )
317
+            body_html = self._build_email_body_for_content(
318
+                self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML,
319
+                role,
320
+                content_in_context,
321
+                workpace_in_context,
322
+                user
323
+            )
302
 
324
 
303
             part1 = MIMEText(body_text, 'plain', 'utf-8')
325
             part1 = MIMEText(body_text, 'plain', 'utf-8')
304
             part2 = MIMEText(body_html, 'html', 'utf-8')
326
             part2 = MIMEText(body_html, 'html', 'utf-8')
362
             'user': user,
384
             'user': user,
363
             'password': password,
385
             'password': password,
364
             # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
386
             # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
365
-            'logo_url': '',
387
+            'logo_url': get_email_logo_frontend_url(self.config),
366
             # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for login_url  # nopep8
388
             # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for login_url  # nopep8
367
-            'login_url': self.config.WEBSITE_BASE_URL,
389
+            'login_url': get_login_frontend_url(self.config),
368
         }
390
         }
369
         body_text = self._render_template(
391
         body_text = self._render_template(
370
             mako_template_filepath=text_template_file_path,
392
             mako_template_filepath=text_template_file_path,
415
             self,
437
             self,
416
             mako_template_filepath: str,
438
             mako_template_filepath: str,
417
             role: UserRoleInWorkspace,
439
             role: UserRoleInWorkspace,
418
-            content: Content,
419
-            actor: User
440
+            content_in_context: ContentInContext,
441
+            workspace_in_context: WorkspaceInContext,
442
+            actor: User,
420
     ) -> str:
443
     ) -> str:
421
         """
444
         """
422
         Build an email body and return it as a string
445
         Build an email body and return it as a string
424
         :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification
447
         :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification
425
         :param content: the content item related to the notification
448
         :param content: the content item related to the notification
426
         :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
449
         :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
427
-        :param config: the global configuration
428
         :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html
450
         :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html
429
         """
451
         """
430
         logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
452
         logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
431
-
453
+        content = content_in_context.content
432
         main_title = content.label
454
         main_title = content.label
433
         content_intro = ''
455
         content_intro = ''
434
         content_text = ''
456
         content_text = ''
435
         call_to_action_text = ''
457
         call_to_action_text = ''
436
 
458
 
437
-        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for call_to_action_url  # nopep8
438
-        call_to_action_url =''
459
+        call_to_action_url = content_in_context.frontend_url
439
         # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
460
         # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
440
         status_icon_url = ''
461
         status_icon_url = ''
441
-        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for workspace_url  # nopep8
442
-        workspace_url = ''
462
+        workspace_url = workspace_in_context.frontend_url
443
         # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
463
         # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
444
-        logo_url = ''
464
+        logo_url = get_email_logo_frontend_url(self.config)
445
 
465
 
446
         action = content.get_last_action().id
466
         action = content.get_last_action().id
447
         if ActionDescription.COMMENT == action:
467
         if ActionDescription.COMMENT == action:

+ 29 - 0
backend/tracim_backend/lib/utils/utils.py View File

2
 import datetime
2
 import datetime
3
 import random
3
 import random
4
 import string
4
 import string
5
+from enum import Enum
6
+
5
 from redis import Redis
7
 from redis import Redis
6
 from rq import Queue
8
 from rq import Queue
7
 
9
 
10
 DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
12
 DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
11
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
13
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
12
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
14
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
15
+CONTENT_FRONTEND_URL_SCHEMA = 'workspaces/{workspace_id}/contents/{content_type}/{content_id}'  # nopep8
16
+WORKSPACE_FRONTEND_URL_SCHEMA = 'workspaces/{workspace_id}'  # nopep8
17
+
18
+
19
+def get_root_frontend_url(config: CFG) -> str:
20
+    """
21
+    Return website base url with always '/' at the end
22
+    """
23
+    base_url = ''
24
+    if config.WEBSITE_BASE_URL[-1] == '/':
25
+        base_url = config.WEBSITE_BASE_URL
26
+    else:
27
+        base_url = config.WEBSITE_BASE_URL + '/'
28
+    return base_url
29
+
30
+
31
+def get_login_frontend_url(config: CFG):
32
+    """
33
+    Return login page url
34
+    """
35
+    return get_root_frontend_url(config) + 'login'
36
+
37
+
38
+def get_email_logo_frontend_url(config: CFG):
39
+    # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for email_logo_frontend_url  # nopep8
40
+    return 'data:image/png;base64,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'  # nopep8'
13
 
41
 
14
 
42
 
15
 def get_redis_connection(config: CFG) -> Redis:
43
 def get_redis_connection(config: CFG) -> Redis:
96
     :return: password as string
124
     :return: password as string
97
     """
125
     """
98
     return ''.join(random.choice(chars) for char_number in range(length))
126
     return ''.join(random.choice(chars) for char_number in range(length))
127
+

+ 6 - 0
backend/tracim_backend/models/applications.py View File

36
         self.config = config
36
         self.config = config
37
         self.main_route = main_route
37
         self.main_route = main_route
38
 
38
 
39
+    # TODO - G.M - 2018-08-07 - Refactor slug coherence issue like this one.
40
+    # we probably should not have 2 kind of slug
41
+    @property
42
+    def minislug(self):
43
+        return self.slug.replace('contents/', '')
44
+
39
 
45
 
40
 # default apps
46
 # default apps
41
 calendar = Application(
47
 calendar = Application(

+ 23 - 2
backend/tracim_backend/models/context_models.py View File

5
 
5
 
6
 from slugify import slugify
6
 from slugify import slugify
7
 from sqlalchemy.orm import Session
7
 from sqlalchemy.orm import Session
8
-from tracim_backend import CFG
8
+from tracim_backend.config import CFG
9
 from tracim_backend.config import PreviewDim
9
 from tracim_backend.config import PreviewDim
10
+from tracim_backend.lib.utils.utils import get_root_frontend_url
11
+from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
12
+from tracim_backend.lib.utils.utils import WORKSPACE_FRONTEND_URL_SCHEMA
10
 from tracim_backend.models import User
13
 from tracim_backend.models import User
11
 from tracim_backend.models.auth import Profile
14
 from tracim_backend.models.auth import Profile
12
 from tracim_backend.models.data import Content
15
 from tracim_backend.models.data import Content
14
 from tracim_backend.models.data import Workspace
17
 from tracim_backend.models.data import Workspace
15
 from tracim_backend.models.data import UserRoleInWorkspace
18
 from tracim_backend.models.data import UserRoleInWorkspace
16
 from tracim_backend.models.roles import WorkspaceRoles
19
 from tracim_backend.models.roles import WorkspaceRoles
17
-from tracim_backend.models.workspace_menu_entries import default_workspace_menu_entry
20
+from tracim_backend.models.workspace_menu_entries import default_workspace_menu_entry  # nopep8
18
 from tracim_backend.models.workspace_menu_entries import WorkspaceMenuEntry
21
 from tracim_backend.models.workspace_menu_entries import WorkspaceMenuEntry
19
 from tracim_backend.models.contents import CONTENT_TYPES
22
 from tracim_backend.models.contents import CONTENT_TYPES
20
 
23
 
462
         # apps)
465
         # apps)
463
         return default_workspace_menu_entry(self.workspace)
466
         return default_workspace_menu_entry(self.workspace)
464
 
467
 
468
+    @property
469
+    def frontend_url(self):
470
+        root_frontend_url = get_root_frontend_url(self.config)
471
+        workspace_frontend_url = WORKSPACE_FRONTEND_URL_SCHEMA.format(
472
+            workspace_id=self.workspace_id,
473
+        )
474
+        return root_frontend_url + workspace_frontend_url
475
+
465
 
476
 
466
 class UserRoleWorkspaceInContext(object):
477
 class UserRoleWorkspaceInContext(object):
467
     """
478
     """
660
         assert self._user
671
         assert self._user
661
         return not self.content.has_new_information_for(self._user)
672
         return not self.content.has_new_information_for(self._user)
662
 
673
 
674
+    @property
675
+    def frontend_url(self):
676
+        root_frontend_url = get_root_frontend_url(self.config)
677
+        content_frontend_url = CONTENT_FRONTEND_URL_SCHEMA.format(
678
+            workspace_id=self.workspace_id,
679
+            content_type=self.content_type,
680
+            content_id=self.content_id,
681
+        )
682
+        return root_frontend_url + content_frontend_url
683
+
663
 
684
 
664
 class RevisionInContext(object):
685
 class RevisionInContext(object):
665
     """
686
     """

+ 3 - 1
backend/tracim_backend/templates/mail/content_update_body_html.mak View File

45
             ${main_title}
45
             ${main_title}
46
             &mdash;&nbsp;<span style="font-weight: bold; color: #999; font-weight: bold;">
46
             &mdash;&nbsp;<span style="font-weight: bold; color: #999; font-weight: bold;">
47
               ${status_label|n}
47
               ${status_label|n}
48
-              <img alt="status_icon" src="${status_icon_url}" style="vertical-align: middle;">
48
+              <img alt="" src="${status_icon_url}" style="vertical-align: middle;">
49
             </span>
49
             </span>
50
         </td>
50
         </td>
51
       </tr>
51
       </tr>
55
     <div id="content-body">
55
     <div id="content-body">
56
         <div>${content_text|n}</div>
56
         <div>${content_text|n}</div>
57
         <div href='' id="call-to-action-container">
57
         <div href='' id="call-to-action-container">
58
+            <span style=""> <a href="${call_to_action_url}"
59
+            id="call-to-action-button">${call_to_action_text}</a> </span> </div>
58
         </div>
60
         </div>
59
     </div>
61
     </div>
60
     
62
     

+ 1 - 2
backend/tracim_backend/templates/mail/created_account_body_html.mak View File

68
         </div>
68
         </div>
69
         <div id="call-to-action-container">
69
         <div id="call-to-action-container">
70
 
70
 
71
-            ${_('To go to {website_title}, please click on following link'.format(
71
+            ${_('To go to {website_title}, please click on following link :'.format(
72
                 website_title=config.WEBSITE_TITLE
72
                 website_title=config.WEBSITE_TITLE
73
             ))}
73
             ))}
74
-
75
             <span style="">
74
             <span style="">
76
                 <a href="${login_url}" id='call-to-action-button'>${login_url}</a>
75
                 <a href="${login_url}" id='call-to-action-button'>${login_url}</a>
77
             </span>
76
             </span>

+ 1 - 0
backend/tracim_backend/tests/library/test_webdav.py View File

29
         :return:
29
         :return:
30
         """
30
         """
31
         tracim_settings = {
31
         tracim_settings = {
32
+            'website.base_url': 'http://localhost:6543',
32
             'sqlalchemy.url': 'sqlite:///:memory:',
33
             'sqlalchemy.url': 'sqlite:///:memory:',
33
             'user.auth_token.validity': '604800',
34
             'user.auth_token.validity': '604800',
34
             'depot_storage_dir': '/tmp/test/depot',
35
             'depot_storage_dir': '/tmp/test/depot',

+ 1 - 1
backend/tracim_backend/views/core_api/schemas.py View File

48
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
48
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
49
     avatar_url = marshmallow.fields.Url(
49
     avatar_url = marshmallow.fields.Url(
50
         allow_none=True,
50
         allow_none=True,
51
-        example="/api/v2/assets/avatars/suri-cate.jpg",
51
+        example="/api/v2/asset/avatars/suri-cate.jpg",
52
         description="avatar_url is the url to the image file. "
52
         description="avatar_url is the url to the image file. "
53
                     "If no avatar, then set it to null "
53
                     "If no avatar, then set it to null "
54
                     "(and frontend will interpret this with a default avatar)",
54
                     "(and frontend will interpret this with a default avatar)",

+ 0 - 1
backend/tracim_backend/views/core_api/user_controller.py View File

39
 SWAGGER_TAG__USER_ENDPOINTS = 'Users'
39
 SWAGGER_TAG__USER_ENDPOINTS = 'Users'
40
 
40
 
41
 
41
 
42
-
43
 class UserController(Controller):
42
 class UserController(Controller):
44
 
43
 
45
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
44
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])

+ 67 - 0
backend/tracim_backend/views/frontend.py View File

1
+import os
2
+
3
+from pyramid.renderers import render_to_response
4
+from pyramid.config import Configurator
5
+from tracim_backend.exceptions import PageNotFound
6
+from tracim_backend.models.applications import applications
7
+from tracim_backend.views import BASE_API_V2
8
+from tracim_backend.lib.utils.request import TracimRequest
9
+from tracim_backend.views.controllers import Controller
10
+import spectra
11
+
12
+INDEX_PAGE_NAME = 'index.mak'
13
+APP_FRONTEND_PATH = 'app/{minislug}.app.js'
14
+
15
+
16
+class FrontendController(Controller):
17
+
18
+    def __init__(self, dist_folder_path: str):
19
+        self.dist_folder_path = dist_folder_path
20
+
21
+    def _get_index_file_path(self):
22
+        index_file_path = os.path.join(self.dist_folder_path, INDEX_PAGE_NAME)
23
+        if not os.path.exists(index_file_path):
24
+            raise FileNotFoundError()
25
+        return index_file_path
26
+
27
+    def not_found_view(self, context, request: TracimRequest):
28
+
29
+        if request.path.startswith(BASE_API_V2):
30
+            raise PageNotFound('{} is not a valid path'.format(request.path)) from context  # nopep8
31
+        return self.index(context, request)
32
+
33
+    def index(self, context, request: TracimRequest):
34
+        app_config = request.registry.settings['CFG']
35
+        # TODO - G.M - 2018-08-07 - Refactor autogen valid app list for frontend
36
+        frontend_apps = []
37
+        for app in applications:
38
+            app_frontend_path = APP_FRONTEND_PATH.replace('{minislug}',
39
+                                                          app.minislug)  # nopep8
40
+            app_path = os.path.join(self.dist_folder_path,
41
+                                    app_frontend_path)  # nopep8
42
+            if os.path.exists(app_path):
43
+                frontend_apps.append(app)
44
+        return render_to_response(
45
+            self._get_index_file_path(),
46
+            {
47
+                'colors': {
48
+                    'primary': spectra.html('#7d4e24'),
49
+                },
50
+                'applications': frontend_apps,
51
+            }
52
+        )
53
+
54
+    def bind(self, configurator: Configurator) -> None:
55
+
56
+        configurator.add_notfound_view(self.not_found_view)
57
+        # index.html for /index.html and /
58
+        configurator.add_route('root', '/', request_method='GET')
59
+        configurator.add_view(self.index, route_name='root')
60
+        configurator.add_route('index', INDEX_PAGE_NAME, request_method='GET')
61
+        configurator.add_view(self.index, route_name='index')
62
+
63
+        for dirname in os.listdir(self.dist_folder_path):
64
+            configurator.add_static_view(
65
+                name=dirname,
66
+                path=os.path.join(self.dist_folder_path, dirname)
67
+            )

+ 0 - 68
frontend/dist/index.html View File

1
-<!DOCTYPE html>
2
-<html>
3
-  <head>
4
-    <meta charset='utf-8' />
5
-    <meta name="viewport" content="width=device-width, user-scalable=no">
6
-    <title>Tracim</title>
7
-    <link rel='shortcut icon' type='image/x-icon' href='/asset/favicon.ico' >
8
-
9
-    <link rel="stylesheet" type="text/css" href="/asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
-    <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-    <!--
12
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
13
-    -->
14
-    <link rel="stylesheet" type="text/css" href="/asset/hamburger/hamburgers.min.css">
15
-    <link rel="stylesheet" type="text/css" href="/asset/bootstrap/bootstrap-4.0.0-beta.css">
16
-
17
-    <style>
18
-      /* code bellow will be generated by backend */
19
-      .primaryColorFont { color: #7d4e24; }
20
-      .primaryColorFontDarken { color: #572800; }
21
-      .primaryColorFontLighten { color: #a3744a; }
22
-
23
-      .primaryColorFontHover:hover { color: #7d4e24; }
24
-      .primaryColorFontDarkenHover:hover { color: #572800; }
25
-      .primaryColorFontLightenHover:hover { color: #a3744a; }
26
-
27
-      .primaryColorBg { background-color: #7d4e24; }
28
-      .primaryColorBgDarken { background-color: #572800; }
29
-      .primaryColorBgLighten { background-color: #a3744a; }
30
-
31
-      .primaryColorBgHover:hover { background-color: #7d4e24; }
32
-      .primaryColorBgDarkenHover:hover { background-color: #572800; }
33
-      .primaryColorBgLightenHover:hover { background-color: #a3744a; }
34
-
35
-      .primaryColorBorder { border-color: #7d4e24; }
36
-      .primaryColorBorderDarken { border-color: #572800; }
37
-      .primaryColorBorderLighten { border-color: #a3744a; }
38
-
39
-      .primaryColorBorderHover:hover { border-color: #7d4e24; }
40
-      .primaryColorBorderDarkenHover:hover { border-color: #572800; }
41
-      .primaryColorBorderLightenHover:hover { border-color: #a3744a; }
42
-
43
-    </style>
44
-  </head>
45
-
46
-  <body>
47
-    <div id='content'></div>
48
-
49
-    <script type='text/javascript' src='/asset/tracim.vendor.bundle.js'></script>
50
-    <script type='text/javascript' src='/asset/tracim.app.entry.js'></script>
51
-
52
-    <script type='text/javascript' src='/app/workspace.app.js'></script>
53
-    <script type='text/javascript' src='/app/html-document.app.js'></script>
54
-    <script type='text/javascript' src='/app/thread.app.js'></script>
55
-    <!-- <script type='text/javascript' src='/app/file.app.js'></script> -->
56
-    <script type='text/javascript' src='/app/admin_workspace_user.app.js'></script>
57
-
58
-    <script type='text/javascript' src='/asset/bootstrap/jquery-3.2.1.js'></script>
59
-    <script type='text/javascript' src='/asset/bootstrap/popper-1.12.3.js'></script>
60
-    <script type='text/javascript' src='/asset/bootstrap/bootstrap-4.0.0-beta.2.js'></script>
61
-
62
-    <script type='text/javascript' src='/asset/tinymce/js/tinymce/jquery.tinymce.min.js'></script>
63
-    <script type='text/javascript' src='/asset/tinymce/js/tinymce/tinymce.min.js'></script>
64
-
65
-    <script type='text/javascript' src='/asset/tracim/appInterface.js'></script>
66
-    <script type='text/javascript' src='/asset/tracim/tinymceInit.js'></script>
67
-  </body>
68
-</html>

+ 80 - 0
frontend/dist/index.mak View File

1
+<!DOCTYPE html>
2
+<html>
3
+  <head>
4
+    <meta charset='utf-8' />
5
+    <meta name="viewport" content="width=device-width, user-scalable=no">
6
+    <title>Tracim</title>
7
+    <link rel='shortcut icon' type='image/x-icon' href='/asset/favicon.ico' >
8
+
9
+    <link rel="stylesheet" type="text/css" href="/asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
+    <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
+    <!--
12
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
13
+    -->
14
+    <link rel="stylesheet" type="text/css" href="/asset/hamburger/hamburgers.min.css">
15
+    <link rel="stylesheet" type="text/css" href="/asset/bootstrap/bootstrap-4.0.0-beta.css">
16
+
17
+    <style>
18
+      <%
19
+        primary = colors['primary']
20
+        html_class = '.primaryColorFont{state}'
21
+        param = 'color'
22
+        color_change_value = 15
23
+      %>
24
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
25
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
26
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
27
+       <% html_class = '.primaryColorFont{state}Hover.hover' %>
28
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
29
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
30
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
31
+
32
+      <%
33
+        html_class = '.primaryColorBg{state}'
34
+        param = 'background-color'
35
+      %>
36
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
37
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
38
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
39
+      <% html_class = '.primaryColorBg{state}Hover.hover'%>
40
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
41
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
42
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
43
+
44
+      <%
45
+        param = 'border-color'
46
+        html_class = '.primaryColorBorder{state}'
47
+      %>
48
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
49
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
50
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
51
+      <% html_class = '.primaryColorBorder{state}Hover.hover' %>
52
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
53
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
54
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
55
+    </style>
56
+  </head>
57
+
58
+  <body>
59
+    <div id='content'></div>
60
+
61
+    <script type='text/javascript' src='/asset/tracim.vendor.bundle.js'></script>
62
+    <script type='text/javascript' src='/asset/tracim.app.entry.js'></script>
63
+
64
+    <script type='text/javascript' src='/app/workspace.app.js'></script>
65
+    % for app in applications:
66
+    <script type='text/javascript' src='/app/${app.minislug}.app.js'></script>
67
+    %endfor
68
+    <script type='text/javascript' src='/app/admin_workspace_user.app.js'></script>
69
+
70
+    <script type='text/javascript' src='/asset/bootstrap/jquery-3.2.1.js'></script>
71
+    <script type='text/javascript' src='/asset/bootstrap/popper-1.12.3.js'></script>
72
+    <script type='text/javascript' src='/asset/bootstrap/bootstrap-4.0.0-beta.2.js'></script>
73
+
74
+    <script type='text/javascript' src='/asset/tinymce/js/tinymce/jquery.tinymce.min.js'></script>
75
+    <script type='text/javascript' src='/asset/tinymce/js/tinymce/tinymce.min.js'></script>
76
+
77
+    <script type='text/javascript' src='/asset/tracim/appInterface.js'></script>
78
+    <script type='text/javascript' src='/asset/tracim/tinymceInit.js'></script>
79
+  </body>
80
+</html>