diff options
-rw-r--r-- | bitbake/lib/toaster/orm/models.py | 131 | ||||
-rw-r--r--[-rwxr-xr-x] | bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html | 0 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/baseprojectpage.html | 43 | ||||
-rw-r--r--[-rwxr-xr-x] | bitbake/lib/toaster/toastergui/templates/layers.html | 9 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/machines.html | 63 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/project.html | 69 | ||||
-rw-r--r--[-rwxr-xr-x] | bitbake/lib/toaster/toastergui/templates/recipes.html | 0 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/templates/targets.html | 186 | ||||
-rwxr-xr-x | bitbake/lib/toaster/toastergui/views.py | 173 |
9 files changed, 559 insertions, 115 deletions
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py index bb921fc98e..77afe35861 100644 --- a/bitbake/lib/toaster/orm/models.py +++ b/bitbake/lib/toaster/orm/models.py | |||
@@ -402,6 +402,18 @@ class Recipe(models.Model): | |||
402 | bugtracker = models.URLField(blank=True) | 402 | bugtracker = models.URLField(blank=True) |
403 | file_path = models.FilePathField(max_length=255) | 403 | file_path = models.FilePathField(max_length=255) |
404 | 404 | ||
405 | def get_vcs_link_url(self): | ||
406 | if self.layer_version.layer.vcs_web_file_base_url is None: | ||
407 | return "" | ||
408 | return self.layer_version.layer.vcs_web_file_base_url.replace('%path%', self.file_path).replace('%branch%', self.layer_version.up_branch.name) | ||
409 | |||
410 | def get_layersource_view_url(self): | ||
411 | if self.layer_source is None: | ||
412 | return "" | ||
413 | |||
414 | url = self.layer_source.get_object_view(self.layer_version.up_branch, "recipes", self.name) | ||
415 | return url | ||
416 | |||
405 | def __unicode__(self): | 417 | def __unicode__(self): |
406 | return "Recipe " + self.name + ":" + self.version | 418 | return "Recipe " + self.name + ":" + self.version |
407 | 419 | ||
@@ -508,6 +520,11 @@ class LayerIndexLayerSource(LayerSource): | |||
508 | super(LayerIndexLayerSource, self).__init__(args, kwargs) | 520 | super(LayerIndexLayerSource, self).__init__(args, kwargs) |
509 | self.sourcetype = LayerSource.TYPE_LAYERINDEX | 521 | self.sourcetype = LayerSource.TYPE_LAYERINDEX |
510 | 522 | ||
523 | def get_object_view(self, branch, objectype, upid): | ||
524 | if self != branch.layer_source: | ||
525 | raise Exception("Invalid branch specification") | ||
526 | return self.apiurl + "../branch/" + branch.name + "/" + objectype + "/?q=" + str(upid) | ||
527 | |||
511 | def update(self): | 528 | def update(self): |
512 | ''' | 529 | ''' |
513 | Fetches layer, recipe and machine information from remote repository | 530 | Fetches layer, recipe and machine information from remote repository |
@@ -538,99 +555,79 @@ class LayerIndexLayerSource(LayerSource): | |||
538 | return | 555 | return |
539 | 556 | ||
540 | # update branches; only those that we already have names listed in the database | 557 | # update branches; only those that we already have names listed in the database |
541 | whitelist_branch_names = self.branchnames.split(",") | 558 | whitelist_branch_names = map(lambda x: x.name, Branch.objects.all()) |
542 | 559 | ||
543 | branches_info = _get_json_response(apilinks['branches'] | 560 | branches_info = _get_json_response(apilinks['branches'] |
544 | + "?filter=name:%s" % "OR".join(whitelist_branch_names)) | 561 | + "?filter=name:%s" % "OR".join(whitelist_branch_names)) |
545 | for bi in branches_info: | 562 | for bi in branches_info: |
546 | try: | 563 | b, created = Branch.objects.get_or_create(layer_source = self, name = bi['name']) |
547 | b = Branch.objects.get(layer_source = self, name = bi['name']) | 564 | b.up_id = bi['id'] |
548 | b.up_id = bi['id'] | 565 | b.up_date = bi['updated'] |
549 | b.up_date = bi['updated'] | 566 | b.name = bi['name'] |
550 | b.name = bi['name'] | 567 | b.bitbake_branch = bi['bitbake_branch'] |
551 | b.bitbake_branch = bi['bitbake_branch'] | 568 | b.short_description = bi['short_description'] |
552 | b.short_description = bi['short_description'] | 569 | b.save() |
553 | b.save() | ||
554 | except Branch.DoesNotExist: | ||
555 | b = Branch.objects.create( | ||
556 | layer_source = self, | ||
557 | up_id = bi['id'], | ||
558 | up_date = bi['updated'], | ||
559 | name = bi['name'], | ||
560 | bitbake_branch = bi['bitbake_branch'], | ||
561 | short_description = bi['short_description'] | ||
562 | ) | ||
563 | 570 | ||
564 | # update layers | 571 | # update layers |
565 | layers_info = _get_json_response(apilinks['layerItems']) | 572 | layers_info = _get_json_response(apilinks['layerItems']) |
566 | for li in layers_info: | 573 | for li in layers_info: |
567 | try: | 574 | l, created = Layer.objects.get_or_create(layer_source = self, up_id = li['id']) |
568 | l = Layer.objects.get(layer_source = self, | 575 | l.up_date = li['updated'] |
569 | up_id = li['id']) | 576 | l.name = li['name'] |
570 | l.update( | 577 | l.vcs_url = li['vcs_url'] |
571 | up_date = li['updated'], | 578 | l.vcs_web_file_base_url = li['vcs_web_file_base_url'] |
572 | name = li['name'], | 579 | l.summary = li['summary'] |
573 | vcs_url = li['vcs_url'], | 580 | l.description = li['description'] |
574 | vcs_web_file_base_url = li['vcs_url'], | 581 | l.save() |
575 | summary = li['summary'], | ||
576 | description = li['description']) | ||
577 | except Layer.DoesNotExist: | ||
578 | Layer.objects.create(layer_source = self, | ||
579 | up_id = li['id'], | ||
580 | up_date = li['updated'], | ||
581 | name = li['name'], | ||
582 | vcs_url = li['vcs_url'], | ||
583 | vcs_web_file_base_url = li['vcs_url'], | ||
584 | summary = li['summary'], | ||
585 | description = li['description'] | ||
586 | ) | ||
587 | 582 | ||
588 | # update layerbranches/layer_versions | 583 | # update layerbranches/layer_versions |
589 | layerbranches_info = _get_json_response(apilinks['layerBranches'] | 584 | layerbranches_info = _get_json_response(apilinks['layerBranches'] |
590 | + "?filter=branch:%s" % "OR".join(map(lambda x: str(x.up_id), Branch.objects.filter(layer_source = self))) | 585 | + "?filter=branch:%s" % "OR".join(map(lambda x: str(x.up_id), Branch.objects.filter(layer_source = self))) |
591 | ) | 586 | ) |
592 | for lbi in layerbranches_info: | 587 | for lbi in layerbranches_info: |
593 | Layer_Version.objects.get_or_create(layer_source = self, | 588 | lv, created = Layer_Version.objects.get_or_create(layer_source = self, up_id = lbi['id']) |
594 | up_id = lbi['id'], | 589 | |
595 | up_date = lbi['updated'], | 590 | lv.up_date = lbi['updated'] |
596 | layer = Layer.objects.get(layer_source = self, up_id = lbi['layer']), | 591 | lv.layer = Layer.objects.get(layer_source = self, up_id = lbi['layer']) |
597 | up_branch = Branch.objects.get(layer_source = self, up_id = lbi['branch']), | 592 | lv.up_branch = Branch.objects.get(layer_source = self, up_id = lbi['branch']) |
598 | branch = lbi['actual_branch'], | 593 | lv.branch = lbi['actual_branch'] |
599 | commit = lbi['vcs_last_rev'], | 594 | lv.commit = lbi['vcs_last_rev'] |
600 | dirpath = lbi['vcs_subdir']) | 595 | lv.dirpath = lbi['vcs_subdir'] |
596 | lv.save() | ||
597 | |||
601 | 598 | ||
602 | # update machines | 599 | # update machines |
603 | machines_info = _get_json_response(apilinks['machines'] | 600 | machines_info = _get_json_response(apilinks['machines'] |
604 | + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) | 601 | + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) |
605 | ) | 602 | ) |
606 | for mi in machines_info: | 603 | for mi in machines_info: |
607 | Machine.objects.get_or_create(layer_source = self, | 604 | mo, created = Machine.objects.get_or_create(layer_source = self, up_id = mi['id']) |
608 | up_id = mi['id'], | 605 | mo.up_date = mi['updated'] |
609 | up_date = mi['updated'], | 606 | mo.layer_version = Layer_Version.objects.get(layer_source = self, up_id = mi['layerbranch']) |
610 | layer_version = Layer_Version.objects.get(layer_source = self, up_id = mi['layerbranch']), | 607 | mo.name = mi['name'] |
611 | name = mi['name'], | 608 | mo.description = mi['description'] |
612 | description = mi['description']) | 609 | mo.save() |
613 | 610 | ||
614 | # update recipes; paginate by layer version / layer branch | 611 | # update recipes; paginate by layer version / layer branch |
615 | recipes_info = _get_json_response(apilinks['recipes'] | 612 | recipes_info = _get_json_response(apilinks['recipes'] |
616 | + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) | 613 | + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) |
617 | ) | 614 | ) |
618 | for ri in recipes_info: | 615 | for ri in recipes_info: |
619 | Recipe.objects.get_or_create(layer_source = self, | 616 | ro, created = Recipe.objects.get_or_create(layer_source = self, up_id = ri['id']) |
620 | up_id = ri['id'], | 617 | |
621 | up_date = ri['updated'], | 618 | ro.up_date = ri['updated'] |
622 | layer_version = Layer_Version.objects.get(layer_source = self, up_id = mi['layerbranch']), | 619 | ro.layer_version = Layer_Version.objects.get(layer_source = self, up_id = mi['layerbranch']) |
623 | 620 | ||
624 | name = ri['pn'], | 621 | ro.name = ri['pn'] |
625 | version = ri['pv'], | 622 | ro.version = ri['pv'] |
626 | summary = ri['summary'], | 623 | ro.summary = ri['summary'] |
627 | description = ri['description'], | 624 | ro.description = ri['description'] |
628 | section = ri['section'], | 625 | ro.section = ri['section'] |
629 | license = ri['license'], | 626 | ro.license = ri['license'] |
630 | homepage = ri['homepage'], | 627 | ro.homepage = ri['homepage'] |
631 | bugtracker = ri['bugtracker'], | 628 | ro.bugtracker = ri['bugtracker'] |
632 | file_path = ri['filepath'] + ri['filename'] | 629 | ro.file_path = ri['filepath'] + ri['filename'] |
633 | ) | 630 | ro.save() |
634 | 631 | ||
635 | pass | 632 | pass |
636 | 633 | ||
diff --git a/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html b/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html index 5149768517..5149768517 100755..100644 --- a/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html +++ b/bitbake/lib/toaster/toastergui/templates/basebuilddetailpage.html | |||
diff --git a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html new file mode 100644 index 0000000000..54edaaf27c --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html | |||
@@ -0,0 +1,43 @@ | |||
1 | {% extends "base.html" %} | ||
2 | {% load projecttags %} | ||
3 | {% load humanize %} | ||
4 | {% block pagecontent %} | ||
5 | |||
6 | |||
7 | <div class=""> | ||
8 | <!-- Breadcrumbs --> | ||
9 | <div class="section"> | ||
10 | <ul class="breadcrumb" id="breadcrumb"> | ||
11 | <li><a href="{% url 'all-builds' %}">All builds</a></li> | ||
12 | {% block parentbreadcrumb %} | ||
13 | {% if project %} | ||
14 | <li> | ||
15 | <a href="{%url 'project' project.id %}">{{project.name}} | ||
16 | </a> | ||
17 | </li> | ||
18 | {% endif %} | ||
19 | {% endblock %} | ||
20 | {% block localbreadcrumb %}{% endblock %} | ||
21 | </ul> | ||
22 | <script> | ||
23 | $( function () { | ||
24 | $('#breadcrumb > li').append("<span class=\"divider\">→</span>"); | ||
25 | $('#breadcrumb > li:last').addClass("active"); | ||
26 | $('#breadcrumb > li:last > span').remove(); | ||
27 | }); | ||
28 | </script> | ||
29 | </div> | ||
30 | |||
31 | <div class="row-fluid"> | ||
32 | |||
33 | <!-- Begin right container --> | ||
34 | {% block projectinfomain %}{% endblock %} | ||
35 | <!-- End right container --> | ||
36 | |||
37 | |||
38 | </div> | ||
39 | </div> | ||
40 | |||
41 | |||
42 | {% endblock %} | ||
43 | |||
diff --git a/bitbake/lib/toaster/toastergui/templates/layers.html b/bitbake/lib/toaster/toastergui/templates/layers.html index d7d159e1e6..bc6e5a3073 100755..100644 --- a/bitbake/lib/toaster/toastergui/templates/layers.html +++ b/bitbake/lib/toaster/toastergui/templates/layers.html | |||
@@ -1,7 +1,12 @@ | |||
1 | {% extends "base.html" %} | 1 | {% extends "baseprojectpage.html" %} |
2 | {% load projecttags %} | 2 | {% load projecttags %} |
3 | {% load humanize %} | 3 | {% load humanize %} |
4 | {% block pagecontent %} | 4 | |
5 | {% block localbreadcrumb %} | ||
6 | <li>Layers</li> | ||
7 | {% endblock %} | ||
8 | |||
9 | {% block projectinfomain %} | ||
5 | <div class="page-header"> | 10 | <div class="page-header"> |
6 | <h1> | 11 | <h1> |
7 | All layers | 12 | All layers |
diff --git a/bitbake/lib/toaster/toastergui/templates/machines.html b/bitbake/lib/toaster/toastergui/templates/machines.html new file mode 100644 index 0000000000..18e7485d50 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/machines.html | |||
@@ -0,0 +1,63 @@ | |||
1 | {% extends "baseprojectpage.html" %} | ||
2 | {% load projecttags %} | ||
3 | {% load humanize %} | ||
4 | |||
5 | {% block localbreadcrumb %} | ||
6 | <li>Machines</li> | ||
7 | {% endblock %} | ||
8 | |||
9 | {% block projectinfomain %} | ||
10 | <div class="page-header"> | ||
11 | <h1> | ||
12 | All machines | ||
13 | <i class="icon-question-sign get-help heading-help" title="This page lists all the machines compatible with Yocto Project 1.7 'Dxxxx' that Toaster knows about. They include community-created targets suitable for use on top of OpenEmbedded Core and any targets you have imported"></i> | ||
14 | </h1> | ||
15 | </div> | ||
16 | <!--div class="alert"> | ||
17 | <div class="input-append" style="margin-bottom:0px;"> | ||
18 | <input class="input-xxlarge" type="text" placeholder="Search targets" value="browser" /> | ||
19 | <a class="add-on btn"> | ||
20 | <i class="icon-remove"></i> | ||
21 | </a> | ||
22 | <button class="btn" type="button">Search</button> | ||
23 | <a class="btn btn-link" href="#">Show all targets</a> | ||
24 | </div> | ||
25 | </div--> | ||
26 | <div id="target-added" class="alert alert-info lead" style="display:none;"></div> | ||
27 | <div id="target-removed" class="alert alert-info lead" style="display:none;"> | ||
28 | <button type="button" class="close" data-dismiss="alert">×</button> | ||
29 | <strong>1</strong> target deleted from <a href="project-with-targets.html">your project</a>: <a href="#">meta-aarch64</a> | ||
30 | </div> | ||
31 | |||
32 | |||
33 | {% include "basetable_top.html" %} | ||
34 | {% for o in objects %} | ||
35 | <tr class="data"> | ||
36 | <td class="machine"> | ||
37 | {{o.name}} | ||
38 | <a machine="_blank" href="http://layers.openembedded.org/layerindex/branch/master/machines/?q=3g-router-image"><i class="icon-share get-info"></i></a> | ||
39 | </td> | ||
40 | <td class="description">{{o.description}}</td> | ||
41 | <td class="machine-file"> | ||
42 | <code>{{o.file_path}}</code> | ||
43 | <a href="http://github.com/embeddedgeeks/meta-embeddedgeeks/blob/master/machines-core/images/3g-router-image.bb" machine="_blank"><i class="icon-share get-info"></i></a> | ||
44 | </td> | ||
45 | <td class="layer"><a href="#">{{o.layer_version.layer.name}}</a></td> | ||
46 | <td class="source">{{o.layer_source.name}}</td> | ||
47 | <td class="branch">{{o.layer_version.commit}}</td> | ||
48 | <td class="build"> | ||
49 | <a id="build-machine" href="project-with-machines.html?machine=3g-router-image" class="btn btn-block" style="display:none;"> | ||
50 | Build machine | ||
51 | </a> | ||
52 | <a id="add-layer" href="#" class="btn btn-block nopop" title="1 layer added"> | ||
53 | <i class="icon-plus"></i> | ||
54 | Add layer | ||
55 | <i class="icon-question-sign get-help" title="To build this machine, you must first add the meta-embeddedgeeks layer to your project"></i> | ||
56 | </a> | ||
57 | </td> | ||
58 | </tr> | ||
59 | {% endfor %} | ||
60 | |||
61 | {% include "basetable_bottom.html" %} | ||
62 | |||
63 | {% endblock %} | ||
diff --git a/bitbake/lib/toaster/toastergui/templates/project.html b/bitbake/lib/toaster/toastergui/templates/project.html index 3c59fcf2ac..d7bfa2b9de 100644 --- a/bitbake/lib/toaster/toastergui/templates/project.html +++ b/bitbake/lib/toaster/toastergui/templates/project.html | |||
@@ -224,36 +224,14 @@ $(document).ready(function () { | |||
224 | <div id="dependency-alert" class="alert alert-info" style="display:none;"> | 224 | <div id="dependency-alert" class="alert alert-info" style="display:none;"> |
225 | <p><strong>meta-tizen</strong> depends on the layers below. Check the ones you want to add: </p> | 225 | <p><strong>meta-tizen</strong> depends on the layers below. Check the ones you want to add: </p> |
226 | <ul class="unstyled"> | 226 | <ul class="unstyled"> |
227 | <li> | 227 | {% for f in layer_dependency %} |
228 | <label class="checkbox"> | ||
229 | <input checked="checked" type="checkbox"> | ||
230 | meta-efl | ||
231 | </label> | ||
232 | </li> | ||
233 | <li> | ||
234 | <label class="checkbox"> | ||
235 | <input checked="checked" type="checkbox"> | ||
236 | meta-intel | ||
237 | </label> | ||
238 | </li> | ||
239 | <li> | ||
240 | <label class="checkbox"> | ||
241 | <input checked="checked" type="checkbox"> | ||
242 | meta-multimedia | ||
243 | </label> | ||
244 | </li> | ||
245 | <li> | ||
246 | <label class="checkbox"> | ||
247 | <input checked="checked" type="checkbox"> | ||
248 | meta-oe | ||
249 | </label> | ||
250 | </li> | ||
251 | <li> | 228 | <li> |
252 | <label class="checkbox"> | 229 | <label class="checkbox"> |
253 | <input checked="checked" type="checkbox"> | 230 | <input checked="checked" type="checkbox"> |
254 | meta-ruby | 231 | meta-ruby |
255 | </label> | 232 | </label> |
256 | </li> | 233 | </li> |
234 | {% endfor %} | ||
257 | </ul> | 235 | </ul> |
258 | <button id="add-layer-dependencies" class="btn btn-info add-layer">Add layers</button> | 236 | <button id="add-layer-dependencies" class="btn btn-info add-layer">Add layers</button> |
259 | </div> | 237 | </div> |
@@ -315,27 +293,38 @@ $(document).ready(function () { | |||
315 | </div> | 293 | </div> |
316 | 294 | ||
317 | <div class="well well-transparent span4"> | 295 | <div class="well well-transparent span4"> |
296 | |||
318 | <h3> | 297 | <h3> |
319 | Set machine | 298 | Project machine |
320 | <i data-original-title="The machine is the hardware for which you want to build. You can only set one machine per project" class="icon-question-sign get-help heading-help" title=""></i> | 299 | <i class="icon-question-sign get-help heading-help" title="The machine is the hardware for which you want to build. You can only set one machine per project"></i> |
321 | </h3> | 300 | </h3> |
322 | <p class="lead"> | 301 | <p class="lead" id="selected-machine"> {{machine}} |
323 | {{machine}} | 302 | <i id="change-machine" class="icon-pencil"></i> |
324 | <i title="" data-original-title="" class="icon-pencil"></i> | ||
325 | </p> | 303 | </p> |
326 | <h3> | 304 | <form id="select-machine"> |
327 | Set distro | 305 | <div class="alert alert-info"> |
328 | <i data-original-title="When you build an image using the Yocto Project and do not alter the distro, you are creating a Poky distribution" class="icon-question-sign get-help heading-help" title=""></i> | 306 | <strong>Machine changes have a big impact on build outcome.</strong> |
329 | </h3> | 307 | You cannot really compare the builds for the new machine with the previous ones. |
330 | <p class="lead"> | 308 | </div> |
331 | {{distro}} | 309 | <div class="input-append"> |
332 | <i title="" data-original-title="" class="icon-pencil"></i> | 310 | <input type="text" id="machine" autocomplete="off" value="qemux86" data-provide="typeahead" |
311 | data-minLength="1" | ||
312 | data-autocomplete="off" | ||
313 | data-source='[ | ||
314 | ]'> | ||
315 | <button id="apply-change-machine" class="btn" type="button">Save</button> | ||
316 | <a href="#" id="cancel-machine" class="btn btn-link">Cancel</a> | ||
317 | </div> | ||
318 | <p><a href="{% url 'machines' %}" class="link">View all machines</a></p> | ||
319 | </form> | ||
320 | <p class="link-action"> | ||
321 | <a href="{% url 'projectconf' project.id %}" class="link">Edit configuration variables</a> | ||
322 | <i class="icon-question-sign get-help heading-help" title="You can set other project configuration options here. Each option, like everything else in the build system, is a variable - value pair"></i> | ||
333 | </p> | 323 | </p> |
334 | <p class="link-action"> | 324 | |
335 | <a href="{% url 'projectconf' project.id %}" class="link">Edit configuration variables</a> | ||
336 | <i class="icon-question-sign get-help heading-help" title="You can set other project configuration options here. Each option, like everything else in the build system, is a variable - value pair"></i> | ||
337 | </p> | ||
338 | </div> | 325 | </div> |
326 | |||
327 | |||
339 | </div> | 328 | </div> |
340 | 329 | ||
341 | <h2>Project details</h2> | 330 | <h2>Project details</h2> |
diff --git a/bitbake/lib/toaster/toastergui/templates/recipes.html b/bitbake/lib/toaster/toastergui/templates/recipes.html index 791a487a81..791a487a81 100755..100644 --- a/bitbake/lib/toaster/toastergui/templates/recipes.html +++ b/bitbake/lib/toaster/toastergui/templates/recipes.html | |||
diff --git a/bitbake/lib/toaster/toastergui/templates/targets.html b/bitbake/lib/toaster/toastergui/templates/targets.html new file mode 100644 index 0000000000..3afdf0a5e9 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/targets.html | |||
@@ -0,0 +1,186 @@ | |||
1 | {% extends "baseprojectpage.html" %} | ||
2 | {% load projecttags %} | ||
3 | {% load humanize %} | ||
4 | |||
5 | {% block localbreadcrumb %} | ||
6 | <li>Targets</li> | ||
7 | {% endblock %} | ||
8 | |||
9 | {% block projectinfomain %} | ||
10 | <div class="page-header"> | ||
11 | <h1> | ||
12 | All targets | ||
13 | <i class="icon-question-sign get-help heading-help" title="This page lists all the targets compatible with Yocto Project 1.7 'Dxxxx' that Toaster knows about. They include community-created targets suitable for use on top of OpenEmbedded Core and any targets you have imported"></i> | ||
14 | </h1> | ||
15 | </div> | ||
16 | <!--div class="alert"> | ||
17 | <div class="input-append" style="margin-bottom:0px;"> | ||
18 | <input class="input-xxlarge" type="text" placeholder="Search targets" value="browser" /> | ||
19 | <a class="add-on btn"> | ||
20 | <i class="icon-remove"></i> | ||
21 | </a> | ||
22 | <button class="btn" type="button">Search</button> | ||
23 | <a class="btn btn-link" href="#">Show all targets</a> | ||
24 | </div> | ||
25 | </div--> | ||
26 | <div id="target-added" class="alert alert-info lead" style="display:none;"></div> | ||
27 | <div id="target-removed" class="alert alert-info lead" style="display:none;"> | ||
28 | <button type="button" class="close" data-dismiss="alert">×</button> | ||
29 | <strong>1</strong> target deleted from <a href="project-with-targets.html">your project</a>: <a href="#">meta-aarch64</a> | ||
30 | </div> | ||
31 | |||
32 | |||
33 | {% include "basetable_top.html" %} | ||
34 | {% for o in objects %} | ||
35 | <tr class="data"> | ||
36 | <td class="target"> | ||
37 | {{o.name}} ({{o.id}}, {{o.up_id}}) | ||
38 | <a target="_blank" href="{{o.get_layersource_view_url}}"><i class="icon-share get-info"></i></a> | ||
39 | </td> | ||
40 | <td class="version">{{o.version}}</td> | ||
41 | <td class="description">{{o.description}}</td> | ||
42 | <td class="recipe-file"> | ||
43 | <code>{{o.file_path}}</code> | ||
44 | <a href="{{o.get_vcs_link_url}}" target="_blank"><i class="icon-share get-info"></i></a> | ||
45 | </td> | ||
46 | <td class="target-section">{{o.section}}</td> | ||
47 | <td class="license">{{o.license}}</td> | ||
48 | <td class="layer"><a href="#">{{o.layer_version.layer.name}}</a></td> | ||
49 | <td class="source">{{o.layer_source.name}}</td> | ||
50 | <td class="branch">{{o.layer_version.commit}}</td> | ||
51 | <td class="build"> | ||
52 | <a id="build-target" href="project-with-targets.html?target=3g-router-image" class="btn btn-block" style="display:none;"> | ||
53 | Build target | ||
54 | </a> | ||
55 | <a id="add-layer" href="#" class="btn btn-block nopop" title="1 layer added"> | ||
56 | <i class="icon-plus"></i> | ||
57 | Add layer | ||
58 | <i class="icon-question-sign get-help" title="To build this target, you must first add the meta-embeddedgeeks layer to your project"></i> | ||
59 | </a> | ||
60 | </td> | ||
61 | </tr> | ||
62 | {% endfor %} | ||
63 | {% include "basetable_bottom.html" %} | ||
64 | |||
65 | <!-- Modals --> | ||
66 | |||
67 | <!-- 'Layer dependencies modal' --> | ||
68 | <div id="dependencies-message" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true"> | ||
69 | <div class="modal-header"> | ||
70 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button> | ||
71 | <h3>meta-acer dependencies</h3> | ||
72 | </div> | ||
73 | <div class="modal-body"> | ||
74 | <p><strong>meta-acer</strong> depends on some targets that are not added to your project. Select the ones you want to add:</p> | ||
75 | <ul class="unstyled"> | ||
76 | <li> | ||
77 | <label class="checkbox"> | ||
78 | <input type="checkbox" checked="checked"> | ||
79 | meta-android | ||
80 | </label> | ||
81 | </li> | ||
82 | <li> | ||
83 | <label class="checkbox"> | ||
84 | <input type="checkbox" checked="checked"> | ||
85 | meta-oe | ||
86 | </label> | ||
87 | </li> | ||
88 | </ul> | ||
89 | </div> | ||
90 | <div class="modal-footer"> | ||
91 | <button id="add-target-dependencies" type="submit" class="btn btn-primary" data-dismiss="modal" >Add targets</button> | ||
92 | <button class="btn" data-dismiss="modal">Cancel</button> | ||
93 | </div> | ||
94 | </div> | ||
95 | |||
96 | <script src="assets/js/jquery-1.9.1.min.js" type='text/javascript'></script> | ||
97 | <script src="assets/js/jquery.tablesorter.min.js" type='text/javascript'></script> | ||
98 | <script src="assets/js/jquery-ui-1.10.3.custom.min.js"></script> | ||
99 | <script src="assets/js/bootstrap.min.js" type='text/javascript'></script> | ||
100 | <script src="assets/js/prettify.js" type='text/javascript'></script> | ||
101 | <script src="assets/js/jit.js" type='text/javascript'></script> | ||
102 | <script src="assets/js/main.js" type='text/javascript'></script> | ||
103 | |||
104 | <script> | ||
105 | $(document).ready(function() { | ||
106 | |||
107 | //show or hide selected columns on load | ||
108 | $("input:checkbox").each(function(){ | ||
109 | var selectedType = $(this).val(); | ||
110 | if($(this).is(":checked")){ | ||
111 | $("."+selectedType).show(); | ||
112 | } | ||
113 | else{ | ||
114 | $("."+selectedType).hide(); | ||
115 | } | ||
116 | }); | ||
117 | |||
118 | // enable add target button | ||
119 | $('#add-target-with-deps').removeAttr('disabled'); | ||
120 | |||
121 | //edit columns functionality (show / hide table columns) | ||
122 | $("input:checkbox").change(); | ||
123 | $("input:checkbox").change(function(){ | ||
124 | var selectedType = $(this).val(); | ||
125 | if($(this).is(":checked")){ | ||
126 | $("."+selectedType).show(); | ||
127 | } | ||
128 | else{ | ||
129 | $("."+selectedType).hide(); | ||
130 | } | ||
131 | }); | ||
132 | |||
133 | //turn edit columns dropdown into a multi-select menu | ||
134 | $('.dropdown-menu input, .dropdown-menu label').click(function(e) { | ||
135 | e.stopPropagation(); | ||
136 | }); | ||
137 | |||
138 | //show tooltip with applied filter | ||
139 | $('#filtered').tooltip({container:'table', placement:'bottom', delay:{hide:1500}, html:true}); | ||
140 | |||
141 | $('#filtered').click(function() { | ||
142 | $(this).tooltip('hide'); | ||
143 | }); | ||
144 | |||
145 | //show target added tooltip | ||
146 | $("#remove-target, #add-target, #add-target-with-deps2").tooltip({ trigger: 'manual' }); | ||
147 | |||
148 | // add target without dependencies | ||
149 | $("#add-target").click(function(){ | ||
150 | $('#target-removed').hide(); | ||
151 | $('#target-added').html('<button type="button" class="close" data-dismiss="alert">×</button><strong>1</strong> target added to <a href="project-with-targets.html">your project</a>: <a href="#">meta-aarch64</a>').fadeIn(); | ||
152 | $('#add-target').tooltip('show'); | ||
153 | $("#add-target").hide(); | ||
154 | $(".add-targets .tooltip").delay(2000).fadeOut(function(){ | ||
155 | $("#remove-target").delay(300).fadeIn(); | ||
156 | }); | ||
157 | }); | ||
158 | |||
159 | // add target with dependencies | ||
160 | $(document).on("click", "#add-target-dependencies", function() { | ||
161 | $('#target-removed').hide(); | ||
162 | $('#target-added').html('<button type="button" class="close" data-dismiss="alert">×</button><strong>3</strong> targets added to <a href="project-with-targets.html">your project</a>: <a href="#">meta-acer</a> and its dependencies <a href="#">meta-android</a> and <a href="#">meta-oe</a>').delay(400).fadeIn(function(){ | ||
163 | $('#add-target-with-deps').tooltip('show'); | ||
164 | $("#add-target-with-deps, #add-target-with-deps").hide(); | ||
165 | $(".add-targets .tooltip").delay(2000).fadeOut(function(){ | ||
166 | $("#remove-target-with-deps").delay(300).fadeIn(); | ||
167 | }); | ||
168 | }); | ||
169 | }); | ||
170 | |||
171 | // delete target | ||
172 | $("#remove-target").click(function(){ | ||
173 | $('#target-added').hide(); | ||
174 | $('#target-removed').show(); | ||
175 | $('#remove-target').tooltip('show'); | ||
176 | $("#remove-target").hide(); | ||
177 | $(".add-targets .tooltip").delay(2000).fadeOut(function(){ | ||
178 | $("#add-target").delay(300).fadeIn(); | ||
179 | }); | ||
180 | }); | ||
181 | |||
182 | }); | ||
183 | |||
184 | </script> | ||
185 | |||
186 | {% endblock %} | ||
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 86a34adf24..167b687d03 100755 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py | |||
@@ -1761,7 +1761,7 @@ if toastermain.settings.MANAGED: | |||
1761 | from django.contrib.auth.decorators import login_required | 1761 | from django.contrib.auth.decorators import login_required |
1762 | 1762 | ||
1763 | from orm.models import Project, ProjectLayer, ProjectTarget, ProjectVariable | 1763 | from orm.models import Project, ProjectLayer, ProjectTarget, ProjectVariable |
1764 | from orm.models import Branch, LayerSource, ToasterSetting, Release | 1764 | from orm.models import Branch, LayerSource, ToasterSetting, Release, Machine |
1765 | from bldcontrol.models import BuildRequest | 1765 | from bldcontrol.models import BuildRequest |
1766 | 1766 | ||
1767 | import traceback | 1767 | import traceback |
@@ -1770,10 +1770,13 @@ if toastermain.settings.MANAGED: | |||
1770 | 1770 | ||
1771 | # the context processor that supplies data used across all the pages | 1771 | # the context processor that supplies data used across all the pages |
1772 | def managedcontextprocessor(request): | 1772 | def managedcontextprocessor(request): |
1773 | return { | 1773 | ret = { |
1774 | "projects": Project.objects.all(), | 1774 | "projects": Project.objects.all(), |
1775 | "MANAGED" : toastermain.settings.MANAGED | 1775 | "MANAGED" : toastermain.settings.MANAGED |
1776 | } | 1776 | } |
1777 | if 'project' in request.session: | ||
1778 | ret['project'] = request.session['project'] | ||
1779 | return ret | ||
1777 | 1780 | ||
1778 | # new project | 1781 | # new project |
1779 | def newproject(request): | 1782 | def newproject(request): |
@@ -1922,7 +1925,7 @@ if toastermain.settings.MANAGED: | |||
1922 | # define here what parameters the view needs in the GET portion in order to | 1925 | # define here what parameters the view needs in the GET portion in order to |
1923 | # be able to display something. 'count' and 'page' are mandatory for all views | 1926 | # be able to display something. 'count' and 'page' are mandatory for all views |
1924 | # that use paginators. | 1927 | # that use paginators. |
1925 | mandatory_parameters = { 'count': 10, 'page' : 1, 'orderby' : 'layer__name:-' }; | 1928 | mandatory_parameters = { 'count': 10, 'page' : 1, 'orderby' : 'layer__name:+' }; |
1926 | retval = _verify_parameters( request.GET, mandatory_parameters ) | 1929 | retval = _verify_parameters( request.GET, mandatory_parameters ) |
1927 | if retval: | 1930 | if retval: |
1928 | return _redirect_parameters( 'layers', request.GET, mandatory_parameters) | 1931 | return _redirect_parameters( 'layers', request.GET, mandatory_parameters) |
@@ -1945,7 +1948,8 @@ if toastermain.settings.MANAGED: | |||
1945 | context = { | 1948 | context = { |
1946 | 'objects' : layer_info, | 1949 | 'objects' : layer_info, |
1947 | 'objectname' : "layers", | 1950 | 'objectname' : "layers", |
1948 | 'default_orderby' : 'completed_on:-', | 1951 | 'default_orderby' : 'layer__name:+', |
1952 | 'total_count': queryset_with_search.count(), | ||
1949 | 1953 | ||
1950 | 'tablecols' : [ | 1954 | 'tablecols' : [ |
1951 | { 'name': 'Layer', | 1955 | { 'name': 'Layer', |
@@ -1954,8 +1958,10 @@ if toastermain.settings.MANAGED: | |||
1954 | }, | 1958 | }, |
1955 | { 'name': 'Description', | 1959 | { 'name': 'Description', |
1956 | 'dclass': 'span4', | 1960 | 'dclass': 'span4', |
1961 | 'clclass': 'description', | ||
1957 | }, | 1962 | }, |
1958 | { 'name': 'Layer source', | 1963 | { 'name': 'Layer source', |
1964 | 'clclass': 'source', | ||
1959 | 'qhelp': "Where the layer is coming from, for example, if it's part of the OpenEmbedded collection of layers or if it's a layer you have imported", | 1965 | 'qhelp': "Where the layer is coming from, for example, if it's part of the OpenEmbedded collection of layers or if it's a layer you have imported", |
1960 | 'orderfield': _get_toggle_order(request, "layer_source__name"), | 1966 | 'orderfield': _get_toggle_order(request, "layer_source__name"), |
1961 | 'ordericon': _get_toggle_order_icon(request, "layer_source__name"), | 1967 | 'ordericon': _get_toggle_order_icon(request, "layer_source__name"), |
@@ -1967,15 +1973,20 @@ if toastermain.settings.MANAGED: | |||
1967 | }, | 1973 | }, |
1968 | { 'name': 'Git repository URL', | 1974 | { 'name': 'Git repository URL', |
1969 | 'dclass': 'span6', | 1975 | 'dclass': 'span6', |
1976 | 'clclass': 'git-repo', 'hidden': 1, | ||
1970 | 'qhelp': "The Git repository for the layer source code", | 1977 | 'qhelp': "The Git repository for the layer source code", |
1971 | }, | 1978 | }, |
1972 | { 'name': 'Subdirectory', | 1979 | { 'name': 'Subdirectory', |
1980 | 'clclass': 'git-subdir', | ||
1981 | 'hidden': 1, | ||
1973 | 'qhelp': "The layer directory within the Git repository", | 1982 | 'qhelp': "The layer directory within the Git repository", |
1974 | }, | 1983 | }, |
1975 | { 'name': 'Branch, tag o commit', | 1984 | { 'name': 'Branch, tag o commit', |
1985 | 'clclass': 'branch', | ||
1976 | 'qhelp': "The Git branch of the layer. For the layers from the OpenEmbedded source, the branch matches the Yocto Project version you selected for this project", | 1986 | 'qhelp': "The Git branch of the layer. For the layers from the OpenEmbedded source, the branch matches the Yocto Project version you selected for this project", |
1977 | }, | 1987 | }, |
1978 | { 'name': 'Dependencies', | 1988 | { 'name': 'Dependencies', |
1989 | 'clclass': 'dependencies', | ||
1979 | 'qhelp': "Other layers a layer depends upon", | 1990 | 'qhelp': "Other layers a layer depends upon", |
1980 | }, | 1991 | }, |
1981 | { 'name': 'Add | Delete', | 1992 | { 'name': 'Add | Delete', |
@@ -1992,10 +2003,160 @@ if toastermain.settings.MANAGED: | |||
1992 | raise Exception("TODO: implement page #6591") | 2003 | raise Exception("TODO: implement page #6591") |
1993 | 2004 | ||
1994 | def targets(request): | 2005 | def targets(request): |
1995 | raise Exception("TODO: implement page #6592") | 2006 | template = "targets.html" |
2007 | # define here what parameters the view needs in the GET portion in order to | ||
2008 | # be able to display something. 'count' and 'page' are mandatory for all views | ||
2009 | # that use paginators. | ||
2010 | mandatory_parameters = { 'count': 10, 'page' : 1, 'orderby' : 'name:+' }; | ||
2011 | retval = _verify_parameters( request.GET, mandatory_parameters ) | ||
2012 | if retval: | ||
2013 | return _redirect_parameters( 'targets', request.GET, mandatory_parameters) | ||
2014 | |||
2015 | # boilerplate code that takes a request for an object type and returns a queryset | ||
2016 | # for that object type. copypasta for all needed table searches | ||
2017 | (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) | ||
2018 | |||
2019 | queryset_all = Recipe.objects.all() | ||
2020 | if 'project' in request.session: | ||
2021 | queryset_all = queryset_all.filter(Q(layer_version__up_branch__in = Branch.objects.filter(name = request.session['project'].release.name)) | Q(layer_version__build__in = request.session['project'].build_set.all())) | ||
2022 | |||
2023 | queryset_with_search = _get_queryset(Recipe, queryset_all, None, search_term, ordering_string, '-name') | ||
2024 | queryset = _get_queryset(Recipe, queryset_all, filter_string, search_term, ordering_string, '-name') | ||
2025 | |||
2026 | # retrieve the objects that will be displayed in the table; targets a paginator and gets a page range to display | ||
2027 | target_info = _build_page_range(Paginator(queryset, request.GET.get('count', 10)),request.GET.get('page', 1)) | ||
2028 | |||
2029 | |||
2030 | context = { | ||
2031 | 'objects' : target_info, | ||
2032 | 'objectname' : "targets", | ||
2033 | 'default_orderby' : 'name:+', | ||
2034 | 'total_count': queryset_with_search.count(), | ||
2035 | |||
2036 | 'tablecols' : [ | ||
2037 | { 'name': 'Target', | ||
2038 | 'orderfield': _get_toggle_order(request, "name"), | ||
2039 | 'ordericon' : _get_toggle_order_icon(request, "name"), | ||
2040 | }, | ||
2041 | { 'name': 'Target version', | ||
2042 | 'dclass': 'span2', | ||
2043 | }, | ||
2044 | { 'name': 'Description', | ||
2045 | 'dclass': 'span5', | ||
2046 | 'clclass': 'description', | ||
2047 | }, | ||
2048 | { 'name': 'Recipe file', | ||
2049 | 'clclass': 'recipe-file', | ||
2050 | 'hidden': 1, | ||
2051 | 'dclass': 'span5', | ||
2052 | }, | ||
2053 | { 'name': 'Section', | ||
2054 | 'clclass': 'target-section', | ||
2055 | 'hidden': 1, | ||
2056 | }, | ||
2057 | { 'name': 'License', | ||
2058 | 'clclass': 'license', | ||
2059 | 'hidden': 1, | ||
2060 | }, | ||
2061 | { 'name': 'Layer', | ||
2062 | 'clclass': 'layer', | ||
2063 | }, | ||
2064 | { 'name': 'Layer source', | ||
2065 | 'clclass': 'source', | ||
2066 | 'qhelp': "Where the target is coming from, for example, if it's part of the OpenEmbedded collection of targets or if it's a target you have imported", | ||
2067 | 'orderfield': _get_toggle_order(request, "layer_source__name"), | ||
2068 | 'ordericon': _get_toggle_order_icon(request, "layer_source__name"), | ||
2069 | 'filter': { | ||
2070 | 'class': 'target', | ||
2071 | 'label': 'Show:', | ||
2072 | 'options': map(lambda x: (x.name, 'layer_source__pk:' + str(x.id), queryset_with_search.filter(layer_source__pk = x.id).count() ), LayerSource.objects.all()), | ||
2073 | } | ||
2074 | }, | ||
2075 | { 'name': 'Branch, tag or commit', | ||
2076 | 'clclass': 'branch', | ||
2077 | 'hidden': 1, | ||
2078 | }, | ||
2079 | { 'name': 'Build', | ||
2080 | 'dclass': 'span2', | ||
2081 | 'qhelp': "Add or delete targets to / from your project ", | ||
2082 | }, | ||
2083 | |||
2084 | ] | ||
2085 | } | ||
2086 | |||
2087 | return render(request, template, context) | ||
1996 | 2088 | ||
1997 | def machines(request): | 2089 | def machines(request): |
1998 | raise Exception("TODO: implement page #6593") | 2090 | template = "machines.html" |
2091 | # define here what parameters the view needs in the GET portion in order to | ||
2092 | # be able to display something. 'count' and 'page' are mandatory for all views | ||
2093 | # that use paginators. | ||
2094 | mandatory_parameters = { 'count': 10, 'page' : 1, 'orderby' : 'name:+' }; | ||
2095 | retval = _verify_parameters( request.GET, mandatory_parameters ) | ||
2096 | if retval: | ||
2097 | return _redirect_parameters( 'machines', request.GET, mandatory_parameters) | ||
2098 | |||
2099 | # boilerplate code that takes a request for an object type and returns a queryset | ||
2100 | # for that object type. copypasta for all needed table searches | ||
2101 | (filter_string, search_term, ordering_string) = _search_tuple(request, Machine) | ||
2102 | |||
2103 | queryset_all = Machine.objects.all() | ||
2104 | # if 'project' in request.session: | ||
2105 | # queryset_all = queryset_all.filter(Q(layer_version__up_branch__in = Branch.objects.filter(name = request.session['project'].release.name)) | Q(layer_version__build__in = request.session['project'].build_set.all())) | ||
2106 | |||
2107 | queryset_with_search = _get_queryset(Machine, queryset_all, None, search_term, ordering_string, '-name') | ||
2108 | queryset = _get_queryset(Machine, queryset_all, filter_string, search_term, ordering_string, '-name') | ||
2109 | |||
2110 | # retrieve the objects that will be displayed in the table; machines a paginator and gets a page range to display | ||
2111 | machine_info = _build_page_range(Paginator(queryset, request.GET.get('count', 10)),request.GET.get('page', 1)) | ||
2112 | |||
2113 | |||
2114 | context = { | ||
2115 | 'objects' : machine_info, | ||
2116 | 'objectname' : "machines", | ||
2117 | 'default_orderby' : 'name:+', | ||
2118 | 'total_count': queryset_with_search.count(), | ||
2119 | |||
2120 | 'tablecols' : [ | ||
2121 | { 'name': 'Machine', | ||
2122 | 'orderfield': _get_toggle_order(request, "name"), | ||
2123 | 'ordericon' : _get_toggle_order_icon(request, "name"), | ||
2124 | }, | ||
2125 | { 'name': 'Description', | ||
2126 | 'dclass': 'span5', | ||
2127 | 'clclass': 'description', | ||
2128 | }, | ||
2129 | { 'name': 'Machine file', | ||
2130 | 'clclass': 'machine-file', | ||
2131 | 'hidden': 1, | ||
2132 | }, | ||
2133 | { 'name': 'Layer', | ||
2134 | 'clclass': 'layer', | ||
2135 | }, | ||
2136 | { 'name': 'Layer source', | ||
2137 | 'clclass': 'source', | ||
2138 | 'qhelp': "Where the machine is coming from, for example, if it's part of the OpenEmbedded collection of machines or if it's a machine you have imported", | ||
2139 | 'orderfield': _get_toggle_order(request, "layer_source__name"), | ||
2140 | 'ordericon': _get_toggle_order_icon(request, "layer_source__name"), | ||
2141 | 'filter': { | ||
2142 | 'class': 'machine', | ||
2143 | 'label': 'Show:', | ||
2144 | 'options': map(lambda x: (x.name, 'layer_source__pk:' + str(x.id), queryset_with_search.filter(layer_source__pk = x.id).count() ), LayerSource.objects.all()), | ||
2145 | } | ||
2146 | }, | ||
2147 | { 'name': 'Branch, tag or commit', | ||
2148 | 'clclass': 'branch', | ||
2149 | 'hidden': 1, | ||
2150 | }, | ||
2151 | { 'name': 'Select', | ||
2152 | 'dclass': 'span2', | ||
2153 | 'qhelp': "Add or delete machines to / from your project ", | ||
2154 | }, | ||
2155 | |||
2156 | ] | ||
2157 | } | ||
2158 | |||
2159 | return render(request, template, context) | ||
1999 | 2160 | ||
2000 | def projectconf(request, pid): | 2161 | def projectconf(request, pid): |
2001 | raise Exception("TODO: implement page #6588") | 2162 | raise Exception("TODO: implement page #6588") |