StrictDoc Documentation
strictdoc/server/routers/other_router.py
Source file coverage
Path:
strictdoc/server/routers/other_router.py
Lines:
304
Non-empty lines:
273
Non-empty lines covered with requirements:
273 / 273 (100.0%)
Functions:
4
Functions covered by requirements:
4 / 4 (100.0%)
1
"""
2
@relation(SDOC-SRS-111, scope=file)
3
"""
4
 
5
import os
6
import urllib
7
from contextlib import ExitStack
8
from copy import deepcopy
9
from datetime import datetime
10
from typing import Optional
11
 
12
from fastapi import APIRouter
13
from starlette.responses import HTMLResponse, Response
14
 
15
from strictdoc import __version__
16
from strictdoc.core.project_config import ProjectConfig
17
from strictdoc.export.html.document_type import DocumentType
18
from strictdoc.export.html.html_templates import HTMLTemplates
19
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
20
from strictdoc.features.diff_and_changelog.change_container import (
21
    ChangeContainer,
22
)
23
from strictdoc.features.diff_and_changelog.change_generator import (
24
    ChangeGenerator,
25
)
26
from strictdoc.features.diff_and_changelog.diff_screen_results_view_object import (
27
    DiffScreenResultsViewObject,
28
)
29
from strictdoc.features.diff_and_changelog.diff_screen_view_object import (
30
    DiffScreenViewObject,
31
)
32
from strictdoc.features.diff_and_changelog.git_client import GitClient
33
from strictdoc.server.helpers.hierarchical_rw_lock_manager import (
34
    HierarchicalRWLockManager,
35
)
36
from strictdoc.server.routers.main_router import (
37
    HTTP_STATUS_BAD_REQUEST,
38
    HTTP_STATUS_PRECONDITION_FAILED,
39
)
40
 
41
 
42
def create_other_router(
43
    project_config: ProjectConfig,
44
    *,
45
    lock_manager: HierarchicalRWLockManager,
46
) -> APIRouter:
47
    router = APIRouter()
48
 
49
    html_templates = HTMLTemplates.create(
50
        project_config=project_config,
51
        enable_caching=False,
52
        strictdoc_last_update=datetime.today(),
53
    )
54
 
55
    @router.get("/diff")
56
    def get_git_diff(
57
        left_revision: Optional[str] = None,
58
        right_revision: Optional[str] = None,
59
        tab: Optional[str] = None,
60
    ) -> Response:
61
        if not project_config.is_activated_diff():
62
            return Response(
63
                content="The DIFF feature is not activated in the project config.",
64
                status_code=HTTP_STATUS_PRECONDITION_FAILED,
65
            )
66
        if tab is not None:
67
            if tab not in ("diff", "changelog"):
68
                return Response(
69
                    content="The tab= parameter must be either 'diff' or 'changelog'.",
70
                    status_code=HTTP_STATUS_BAD_REQUEST,
71
                )
72
        else:
73
            tab = "diff"
74
 
75
        error_message: Optional[str] = None
76
 
77
        if (
78
            left_revision is not None
79
            and len(left_revision) > 0
80
            and right_revision is not None
81
            and len(right_revision) > 0
82
        ):
83
            git_client = GitClient(".")
84
            try:
85
                if left_revision != "HEAD+":
86
                    git_client.check_revision(left_revision)
87
                else:
88
                    raise LookupError(
89
                        "Left revision argument 'HEAD+' is not supported. "
90
                        "'HEAD+' can only be used as a right revision argument."
91
                    )
92
 
93
                if right_revision != "HEAD+":
94
                    git_client.check_revision(right_revision)
95
            except LookupError as exception_:
96
                error_message = exception_.args[0]
97
        elif (left_revision is not None and len(left_revision) > 0) or (
98
            right_revision is not None and len(right_revision) > 0
99
        ):
100
            error_message = "Valid Git revisions must be provided."
101
        else:
102
            # In the case when both revisions are empty, we load the starting
103
            # diff page.
104
            pass
105
 
106
        view_object = DiffScreenViewObject(
107
            project_config=project_config,
108
            results=False,
109
            left_revision=left_revision,
110
            right_revision=right_revision,
111
            error_message=error_message,
112
            tab=tab,
113
        )
114
        output = view_object.render_screen(html_templates.jinja_environment())
115
        status_code = 200 if error_message is None else 422
116
        return HTMLResponse(content=output, status_code=status_code)
117
 
118
    @router.get("/diff_result")
119
    def get_git_diff_result(
120
        left_revision: Optional[str] = None,
121
        right_revision: Optional[str] = None,
122
        tab: Optional[str] = None,
123
    ) -> Response:
124
        if not project_config.is_activated_diff():
125
            return Response(
126
                content="The DIFF feature is not activated in the project config.",
127
                status_code=HTTP_STATUS_PRECONDITION_FAILED,
128
            )
129
        if tab is not None and tab not in ("diff", "changelog"):
130
            return Response(
131
                content="The tab= parameter must be either 'diff' or 'changelog'.",
132
                status_code=HTTP_STATUS_PRECONDITION_FAILED,
133
            )
134
        elif tab is None:
135
            tab = "diff"
136
 
137
        left_revision_resolved = None
138
        right_revision_resolved = None
139
 
140
        results = False
141
        error_message: Optional[str] = None
142
 
143
        if (
144
            left_revision is not None
145
            and len(left_revision) > 0
146
            and right_revision is not None
147
            and len(right_revision) > 0
148
        ):
149
            git_client = GitClient(".")
150
            try:
151
                if left_revision != "HEAD+":
152
                    left_revision_resolved = git_client.check_revision(
153
                        left_revision
154
                    )
155
                else:
156
                    raise LookupError(
157
                        "Left revision argument 'HEAD+' is not supported. "
158
                        "'HEAD+' can only be used as a right revision argument."
159
                    )
160
 
161
                if right_revision == "HEAD+":
162
                    right_revision_resolved = "HEAD+"
163
                else:
164
                    right_revision_resolved = git_client.check_revision(
165
                        right_revision
166
                    )
167
 
168
                results = True
169
            except LookupError as exception_:
170
                error_message = exception_.args[0]
171
        elif (left_revision is not None and len(left_revision) > 0) or (
172
            right_revision is not None and len(right_revision) > 0
173
        ):
174
            error_message = "Valid Git revisions must be provided."
175
        else:
176
            # In the case when both revisions are empty, we load the starting
177
            # diff page.
178
            pass
179
 
180
        path_to_template = (
181
            "features/diff_and_changelog/frame_changelog_result.jinja"
182
            if tab == "changelog"
183
            else "features/diff_and_changelog/frame_diff_result.jinja"
184
        )
185
        template = html_templates.jinja_environment().get_template(
186
            path_to_template
187
        )
188
 
189
        link_renderer = LinkRenderer(
190
            root_path="", static_path=project_config.dir_for_sdoc_assets
191
        )
192
 
193
        left_revision_urlencoded = (
194
            urllib.parse.quote(left_revision)
195
            if left_revision is not None
196
            else ""
197
        )
198
        right_revision_urlencoded = (
199
            urllib.parse.quote(right_revision)
200
            if right_revision is not None
201
            else ""
202
        )
203
 
204
        if not results:
205
            output = template.render(
206
                project_config=project_config,
207
                document_type=DocumentType.DOCUMENT.value,
208
                link_document_type=DocumentType.DOCUMENT.value,
209
                strictdoc_version=__version__,
210
                link_renderer=link_renderer,
211
                results=False,
212
                left_revision=left_revision,
213
                left_revision_urlencoded=left_revision_urlencoded,
214
                right_revision=right_revision,
215
                right_revision_urlencoded=right_revision_urlencoded,
216
                error_message=error_message,
217
            )
218
            status_code = 200 if error_message is None else 422
219
            return HTMLResponse(content=output, status_code=status_code)
220
 
221
        assert left_revision_resolved is not None
222
        assert right_revision_resolved is not None
223
 
224
        def open_git_client_for_revision(revision: str) -> GitClient:
225
            if revision == "HEAD+":
226
                # Serialize HEAD+ snapshot creation with main router writes.
227
                with lock_manager.acquire_global_write():
228
                    return exit_stack.enter_context(
229
                        GitClient.create_cached_repo_from_local_copy(
230
                            revision, project_config
231
                        )
232
                    )
233
            return exit_stack.enter_context(
234
                GitClient.create_cached_repo_from_local_copy(
235
                    revision, project_config
236
                )
237
            )
238
 
239
        with ExitStack() as exit_stack:
240
            git_client_lhs = open_git_client_for_revision(
241
                left_revision_resolved
242
            )
243
            git_client_rhs = open_git_client_for_revision(
244
                right_revision_resolved
245
            )
246
 
247
            project_config_copy_lhs: ProjectConfig = deepcopy(project_config)
248
            assert project_config_copy_lhs.input_paths is not None
249
            project_config_copy_rhs: ProjectConfig = deepcopy(project_config)
250
            assert project_config_copy_rhs.input_paths is not None
251
 
252
            export_input_rel_path = os.path.relpath(
253
                project_config_copy_lhs.input_paths[0], os.getcwd()
254
            )
255
            export_input_abs_path = os.path.join(
256
                git_client_lhs.path_to_git_root, export_input_rel_path
257
            )
258
            project_config_copy_lhs.input_paths = [export_input_abs_path]
259
 
260
            export_input_rel_path = os.path.relpath(
261
                project_config_copy_rhs.input_paths[0], os.getcwd()
262
            )
263
            export_input_abs_path = os.path.join(
264
                git_client_rhs.path_to_git_root, export_input_rel_path
265
            )
266
            project_config_copy_rhs.input_paths = [export_input_abs_path]
267
 
268
            change_container: ChangeContainer = ChangeGenerator.generate(
269
                lhs_project_config=project_config_copy_lhs,
270
                rhs_project_config=project_config_copy_rhs,
271
            )
272
 
273
            assert (
274
                change_container.traceability_index_lhs.document_tree
275
                is not None
276
            )
277
            assert (
278
                change_container.traceability_index_rhs.document_tree
279
                is not None
280
            )
281
            view_object = DiffScreenResultsViewObject(
282
                project_config=project_config,
283
                change_container=change_container,
284
                document_tree_lhs=change_container.traceability_index_lhs.document_tree,
285
                document_tree_rhs=change_container.traceability_index_rhs.document_tree,
286
                documents_iterator_lhs=change_container.documents_iterator_lhs,
287
                documents_iterator_rhs=change_container.documents_iterator_rhs,
288
                left_revision=left_revision,
289
                right_revision=right_revision,
290
                lhs_stats=change_container.lhs_stats,
291
                rhs_stats=change_container.rhs_stats,
292
                change_stats=change_container.change_stats,
293
                traceability_index_lhs=change_container.traceability_index_lhs,
294
                traceability_index_rhs=change_container.traceability_index_rhs,
295
                tab=tab,
296
            )
297
            output = template.render(view_object=view_object)
298
 
299
        return HTMLResponse(
300
            content=output,
301
            status_code=200,
302
        )
303
 
304
    return router