tags implemented

This commit is contained in:
Daniel Tsvetkov 2021-05-01 16:19:02 +02:00
parent 705904e380
commit 2efd1e357f
40 changed files with 599 additions and 46 deletions

20
data_static/Tag.csv Normal file
View File

@ -0,0 +1,20 @@
name
English
"Български"
"политика"
politics
technology
"технологии"
hacks
"мнения"
opinions
projects
"проекти"
"есета"
essays
"лични"
personal
university
learning
life
"живот"
1 name
2 English
3 Български
4 политика
5 politics
6 technology
7 технологии
8 hacks
9 мнения
10 opinions
11 projects
12 проекти
13 есета
14 essays
15 лични
16 personal
17 university
18 learning
19 life
20 живот

View File

@ -1,2 +1,5 @@
Tag
BlogPost
blog_post__tag
Role
User

View File

@ -0,0 +1,156 @@
blog_post_id,tag_id
1,2
1,3
51,2
51,3
50,1
51,6
51,8
1,8
2,1
2,9
30,1
30,5
30,9
34,2
34,12
43,2
43,3
43,8
53,2
53,3
44,2
44,3
44,12
47,2
47,3
47,8
48,1
48,13
45,2
45,11
31,1
31,15
4,1
4,16
6,1
6,16
9,1
9,16
10,1
10,16
11,1
11,16
12,1
12,16
15,1
15,16
16,1
16,16
17,1
17,16
18,1
18,16
19,1
19,16
20,1
20,16
23,1
23,16
24,1
24,16
25,1
25,16
26,1
26,16
27,1
27,16
29,1
29,16
32,1
32,16
37,1
37,16
39,1
39,16
40,1
40,16
41,1
41,16
42,1
42,7
50,4
49,1
49,17
21,1
21,5
21,7
28,2
28,3
3,1
3,5
3,13
5,1
5,5
5,13
7,1
7,13
7,18
8,1
8,5
8,7
13,1
13,16
14,1
14,5
14,7
22,1
22,13
22,18
33,2
33,8
38,1
38,9
35,1
35,5
35,10
35,17
36,1
36,13
36,15
36,18
46,2
46,6
46,8
52,2
54,2
54,8
55,2
56,2
57,2
57,12
57,14
57,19
58,2
58,14
59,2
59,8
59,19
60,2
60,3
60,8
61,2
61,6
61,8
62,2
62,6
62,8
63,2
63,14
64,2
64,14
64,19
65,2
65,12
66,2
66,8
1 blog_post_id tag_id
2 1 2
3 1 3
4 51 2
5 51 3
6 50 1
7 51 6
8 51 8
9 1 8
10 2 1
11 2 9
12 30 1
13 30 5
14 30 9
15 34 2
16 34 12
17 43 2
18 43 3
19 43 8
20 53 2
21 53 3
22 44 2
23 44 3
24 44 12
25 47 2
26 47 3
27 47 8
28 48 1
29 48 13
30 45 2
31 45 11
32 31 1
33 31 15
34 4 1
35 4 16
36 6 1
37 6 16
38 9 1
39 9 16
40 10 1
41 10 16
42 11 1
43 11 16
44 12 1
45 12 16
46 15 1
47 15 16
48 16 1
49 16 16
50 17 1
51 17 16
52 18 1
53 18 16
54 19 1
55 19 16
56 20 1
57 20 16
58 23 1
59 23 16
60 24 1
61 24 16
62 25 1
63 25 16
64 26 1
65 26 16
66 27 1
67 27 16
68 29 1
69 29 16
70 32 1
71 32 16
72 37 1
73 37 16
74 39 1
75 39 16
76 40 1
77 40 16
78 41 1
79 41 16
80 42 1
81 42 7
82 50 4
83 49 1
84 49 17
85 21 1
86 21 5
87 21 7
88 28 2
89 28 3
90 3 1
91 3 5
92 3 13
93 5 1
94 5 5
95 5 13
96 7 1
97 7 13
98 7 18
99 8 1
100 8 5
101 8 7
102 13 1
103 13 16
104 14 1
105 14 5
106 14 7
107 22 1
108 22 13
109 22 18
110 33 2
111 33 8
112 38 1
113 38 9
114 35 1
115 35 5
116 35 10
117 35 17
118 36 1
119 36 13
120 36 15
121 36 18
122 46 2
123 46 6
124 46 8
125 52 2
126 54 2
127 54 8
128 55 2
129 56 2
130 57 2
131 57 12
132 57 14
133 57 19
134 58 2
135 58 14
136 59 2
137 59 8
138 59 19
139 60 2
140 60 3
141 60 8
142 61 2
143 61 6
144 61 8
145 62 2
146 62 6
147 62 8
148 63 2
149 63 14
150 64 2
151 64 14
152 64 19
153 65 2
154 65 12
155 66 2
156 66 8

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,41 +0,0 @@
"""002
Revision ID: 28f9ab4fc8b0
Revises: d08a068031c3
Create Date: 2020-07-08 14:27:11.649888
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '28f9ab4fc8b0'
down_revision = 'd08a068031c3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('blog_post',
sa.Column('created_dt', sa.UnicodeText(), nullable=True),
sa.Column('updated_dt', sa.UnicodeText(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.Unicode(), nullable=True),
sa.Column('filename', sa.UnicodeText(), nullable=True),
sa.Column('title', sa.UnicodeText(), nullable=True),
sa.Column('body', sa.UnicodeText(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_blog_post_uuid'), 'blog_post', ['uuid'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_blog_post_uuid'), table_name='blog_post')
op.drop_table('blog_post')
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""001
Revision ID: d08a068031c3
Revision ID: d091fbf48f6f
Revises:
Create Date: 2020-07-08 11:48:15.369556
Create Date: 2021-05-01 11:54:58.288320
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd08a068031c3'
revision = 'd091fbf48f6f'
down_revision = None
branch_labels = None
depends_on = None
@ -53,18 +53,51 @@ def upgrade():
sa.UniqueConstraint('email')
)
op.create_index(op.f('ix_user_uuid'), 'user', ['uuid'], unique=False)
op.create_table('blog_post',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.Unicode(), nullable=True),
sa.Column('filename', sa.UnicodeText(), nullable=True),
sa.Column('title', sa.UnicodeText(), nullable=True),
sa.Column('body', sa.UnicodeText(), nullable=True),
sa.Column('created_dt', sa.UnicodeText(), nullable=True),
sa.Column('updated_dt', sa.UnicodeText(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_blog_post_uuid'), 'blog_post', ['uuid'], unique=False)
op.create_table('roles_users',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
)
op.create_table('tag',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.Unicode(), nullable=True),
sa.Column('name', sa.UnicodeText(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tag_uuid'), 'tag', ['uuid'], unique=False)
op.create_table('blog_post__tag',
sa.Column('blog_post_id', sa.Integer(), nullable=True),
sa.Column('tag_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['blog_post_id'], ['blog_post.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], )
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('blog_post__tag')
op.drop_index(op.f('ix_tag_uuid'), table_name='tag')
op.drop_table('tag')
op.drop_table('roles_users')
op.drop_index(op.f('ix_blog_post_uuid'), table_name='blog_post')
op.drop_table('blog_post')
op.drop_index(op.f('ix_user_uuid'), table_name='user')
op.drop_table('user')
op.drop_table('task')

View File

@ -1,2 +1,3 @@
from oshipka.persistance import db
from webapp.models.tag import Tag
from webapp.models.blog_post import BlogPost

View File

@ -1,5 +1,10 @@
from oshipka.persistance import db, ModelController, index_service, LiberalBoolean, Ownable
blog_post__tag = db.Table('blog_post__tag',
db.Column('blog_post_id', db.Integer(), db.ForeignKey('blog_post.id')),
db.Column('tag_id', db.Integer(), db.ForeignKey('tag.id')),
)
class BlogPost(db.Model, ModelController, Ownable):
__searchable__ = ['body', ]
@ -7,6 +12,11 @@ class BlogPost(db.Model, ModelController, Ownable):
filename = db.Column(db.UnicodeText,)
title = db.Column(db.UnicodeText,)
body = db.Column(db.UnicodeText,)
tags = db.relationship('Tag', secondary=blog_post__tag,
backref=db.backref("blog_posts"),
)
_m_n_table_tags = 'Tag'
created_dt = db.Column(db.UnicodeText,)
updated_dt = db.Column(db.UnicodeText,)

8
webapp/models/_tag.py Normal file
View File

@ -0,0 +1,8 @@
from oshipka.persistance import db, ModelController, index_service, LiberalBoolean, Ownable
class Tag(db.Model, ModelController, Ownable):
name = db.Column(db.UnicodeText,)
def __repr__(self):
return "{}".format(self.name)

1
webapp/models/tag.py Normal file
View File

@ -0,0 +1 @@
from webapp.models._tag import Tag

View File

@ -1 +1,2 @@
from webapp.routes.tag import *
from webapp.routes.blog_post import *

68
webapp/routes/tag.py Normal file
View File

@ -0,0 +1,68 @@
"""
!!!AUTOGENERATED: DO NOT EDIT!!!
Edit the hooks in webapp/routes/tag_hooks.py instead
"""
from oshipka.webapp import app
from oshipka.webapp.views import ModelView
from webapp.models import Tag
from webapp.routes.tag_hooks import *
tag = ModelView(app, Tag)
tag.register_verb(view_context=get_view_context,
verb="get",
methods=['GET'],
per_item=True,
is_login_required=False,
the_roles_required=[],
)
tag.register_verb(view_context=list_view_context,
verb="list",
methods=['GET'],
per_item=False,
is_login_required=False,
the_roles_required=[],
)
tag.register_verb(view_context=table_view_context,
verb="table",
methods=['GET'],
per_item=False,
is_login_required=False,
the_roles_required=[],
)
tag.register_verb(view_context=search_view_context,
verb="search",
methods=['GET'],
per_item=False,
is_login_required=False,
the_roles_required=[],
)
tag.register_verb(view_context=create_view_context,
verb="create",
methods=['GET', 'POST'],
per_item=False,
is_login_required=True,
the_roles_required=['admin'],
)
tag.register_verb(view_context=update_view_context,
verb="update",
methods=['GET', 'POST'],
per_item=True,
is_login_required=True,
the_roles_required=['admin'],
)
tag.register_verb(view_context=delete_view_context,
verb="delete",
methods=['GET', 'POST'],
per_item=True,
is_login_required=True,
the_roles_required=['admin'],
)

View File

@ -0,0 +1,71 @@
from oshipka.webapp.views import ViewContext, default_get_args_func, default_get_func, default_list_func, \
default_get_form_func, default_create_func, default_update_func, default_delete_func, default_search_func
def get_template(vc):
vc.template = "{}/get.html".format(vc.model_view.model_name)
def list_template(vc):
vc.template = "{}/list.html".format(vc.model_view.model_name)
def table_template(vc):
vc.template = "{}/table.html".format(vc.model_view.model_name)
def search_template(vc):
vc.template = "{}/search.html".format(vc.model_view.model_name)
def create_template(vc):
vc.template = "{}/create.html".format(vc.model_view.model_name)
def update_template(vc):
vc.template = "{}/update.html".format(vc.model_view.model_name)
def delete_template(vc):
vc.template = "delete_instance.html".format(vc.model_view.model_name)
get_view_context = ViewContext(
filter_func=default_get_func,
template_func=get_template,
)
list_view_context = ViewContext(
filter_func=default_list_func,
template_func=list_template,
)
table_view_context = ViewContext(
filter_func=default_list_func,
template_func=table_template,
)
search_view_context = ViewContext(
filter_func=default_search_func,
template_func=list_template,
)
create_view_context = ViewContext(
args_get_func=default_get_form_func,
template_func=create_template,
execute_func=default_create_func,
)
update_view_context = ViewContext(
args_get_func=default_get_form_func,
filter_func=default_get_func,
template_func=update_template,
execute_func=default_update_func,
)
delete_view_context = ViewContext(
args_get_func=default_get_form_func,
filter_func=default_get_func,
template_func=delete_template,
execute_func=default_delete_func,
)

View File

@ -25,6 +25,24 @@
</td><td>
<textarea id="input-blog_post-body"
name="body"></textarea>
</td></tr>
{% endif %}
{% if "tag" not in disabled_columns %}
<tr {% if "tag" in hidden_columns %}style="display: none;"{% endif %}><td>
<label for="input-blog_post-tag">{{ _("tag") }}</label>:
</td><td>
<select id="input-blog_post-tag" name="_m_n_tags" multiple>
{% if tags is not defined and instance and instance.tags is defined %}
{% set tags = instance.tags %}
{% else %}
{% set tags = model_views.tag.model.query.all() %}
{% endif %}
{%- for sub_instance in tags %}
<option value="{{ sub_instance.id }}" {% if model_view.model_name == "tag" and instance and instance.id == sub_instance.id %}selected="selected"{% endif %}>{{ sub_instance }}</option>
{%- endfor %}
</select>
</td></tr>
{% endif %}
{% if "created_dt" not in disabled_columns %}

View File

@ -8,6 +8,9 @@
{% if "body" not in skip_list %}
<li id="display-blog_post-body"><strong>{{ _("body") }}</strong>: {{ instance.body }}</li>
{% endif %}
{% if "tag" not in skip_list %}
<li id="display-blog_post-tag"><strong>{{ _("tags") }}</strong>: {{ instance.tags }}</li>
{% endif %}
{% if "created_dt" not in skip_list %}
<li id="display-blog_post-created_dt"><strong>{{ _("created_dt") }}</strong>: {{ instance.created_dt }}</li>
{% endif %}

View File

@ -2,4 +2,13 @@
<a href="{{ url_for('get_blog_post', uuid=instance.id) }}">
{% include "blog_post/_title.html" %}</a>
{% include "blog_post/_actions.html" %}
{% if instance.tags %}
<ul>
<li>
<small><i>{{ _("Tags") }}: {% for tag in instance.tags %}
<a href="{{ url_for('get_tag', uuid=tag.id) }}">{{ tag.name }}</a>{% if not loop.last %},
{% endif %} {% endfor %}</i></small><br>
</li>
</ul>
{% endif %}
</li>

View File

@ -10,6 +10,9 @@
{% if "body" not in skip_columns %}
<th>{{ _("body") }}</th>
{% endif %}
{% if "tag" not in skip_columns %}
<th>{{ _("tag") }}</th>
{% endif %}
{% if "created_dt" not in skip_columns %}
<th>{{ _("created_dt") }}</th>
{% endif %}
@ -37,6 +40,11 @@
{{ instance.body }}
</td>
{% endif %}
{% if "tag" not in skip_columns %}
<td>
{{ instance.tags }}
</td>
{% endif %}
{% if "created_dt" not in skip_columns %}
<td>
{{ instance.created_dt }}

View File

@ -27,6 +27,19 @@
</td><td>
<textarea id="input-blog_post-body"
name="body">{{ instance.body }}</textarea>
</td></tr>
{% endif %}
{% if "tag" not in disabled_columns %}
<tr {% if "tag" in hidden_columns %}style="display: none;"{% endif %}><td>
<label for="input-blog_post-tag">{{ _("tag") }}</label>:
</td><td>
<input type="hidden" name="_m_n_tags" value="" />
<select id="input-blog_post-tag" name="_m_n_tags" multiple>
{%- for sub_instance in model_views.tag.model.query.all() %}
<option value="{{ sub_instance.id }}"
{% if sub_instance in instance.tags %}selected="selected"{% endif %}>{{ sub_instance }}</option>
{%- endfor %}
</select>
</td></tr>
{% endif %}
{% if "created_dt" not in disabled_columns %}

View File

@ -1,3 +1,7 @@
<h2><a href="{{ url_for('blog_post_filename', filename=instance.filename) }}">{{ instance.title }}</a></h2>
<small><i>{{ _("Created on") }} {{ instance.created_dt }}</i></small>
{% if instance.tags %}
<small><i>{{ _("Tags") }}: {% for tag in instance.tags %}
<a href="{{ url_for('get_tag', uuid=tag.id) }}">{{ tag.name }}</a>{% if not loop.last %},{% endif %} {% endfor %}</i></small><br>
{% endif %}
<small><i>{{ _("Created on") }} {{ instance.created_dt|to_dt|format_dt }}</i></small>
{{ instance.body|markdown|rawhtmlparse|safe }}

View File

@ -1,4 +1,9 @@
<a href="{{ url_for('home') }}">{{ _("PiSquared Blog") }}</a> |
<a href="{{ url_for('index') }}">{{ _("Index") }}</a> |
<a href="{{ url_for('list_tag') }}">{{ _("Tags") }}</a> |
<a href="{{ url_for('rss') }}">{{ _("RSS") }}</a> |
<a href="{{ url_for('aboutme') }}">{{ _("About Me") }}</a>
<a href="{{ url_for('aboutme') }}">{{ _("About Me") }}</a>
{% if current_user.is_authenticated %}
| <a href="{{ url_for('list_blog_post') }}" style="background-color: red;">{{ _("Admin") }}</a>
{% endif %}

View File

@ -0,0 +1 @@
<a href="{{ url_for('delete_tag', uuid=instance.id, _next=request.path) }}">x</a>

View File

@ -0,0 +1 @@
<a href="{{ url_for('update_tag', uuid=instance.id, _next=request.path) }}">e</a>

View File

@ -0,0 +1,4 @@
[
{% include "tag/_action_edit.html" %} |
{% include "tag/_action_delete.html" %}
]

View File

@ -0,0 +1,15 @@
<form action="{{ url_for('create_tag') }}" method="post">
<input type="hidden" name="_next" value="{{ _next or request.args.get('_next') or url_for('list_tag') }}"/>
<table>
{% if "name" not in disabled_columns %}
<tr {% if "name" in hidden_columns %}style="display: none;"{% endif %}><td>
<label for="input-tag-name">{{ _("name") }}</label>:
</td><td>
<input id="input-tag-name"
type="text" name="name" autocomplete="off"
/>
</td></tr>
{% endif %}
</table>
<input type="submit">
</form>

View File

@ -0,0 +1,4 @@
{% if "name" not in skip_list %}
<li id="display-tag-name"><strong>{{ _("name") }}</strong>: {{ instance.name }}</li>
{% endif %}

View File

@ -0,0 +1,3 @@
{% for instance in instances %}
{% include "tag/_list_item.html" %}
{% endfor %}

View File

@ -0,0 +1,7 @@
<li>
<a href="{{ url_for('get_tag', uuid=instance.id) }}">
{% include "tag/_title.html" %}</a> ({{ instance.blog_posts|count }})
{% if current_user.is_authenticated %}
{% include "tag/_actions.html" %}
{% endif %}
</li>

View File

@ -0,0 +1,11 @@
{% for instance in instances %}
<li>
<a href="{{ url_for('get_tag', uuid=instance.id) }}">
{% include "tag/_title.html" %}</a>
|
[
<a href="{{ url_for('update_tag', uuid=instance.id) }}">e</a> |
<a href="{{ url_for('delete_tag', uuid=instance.id) }}">x</a>
]
</li>
{% endfor %}

View File

@ -0,0 +1,25 @@
<table class="full-width data-table">
<thead>
<tr>
{% if "name" not in skip_columns %}
<th>{{ _("name") }}</th>
{% endif %}
<th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for instance in instances %}
<tr>
{% if "name" not in skip_columns %}
<td>
{{ instance.name }}
</td>
{% endif %}
<td>
<a href="{{ url_for('update_tag', uuid=instance.id, _next=request.path) }}">e</a> |
<a href="{{ url_for('delete_tag', uuid=instance.id, _next=request.path) }}">x</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,2 @@
{{ instance.name }}

View File

@ -0,0 +1,16 @@
<form action="{{ url_for('update_tag', uuid=instance.id) }}" method="post">
<input type="hidden" name="_next" value="{{ _next or request.args.get('_next') or url_for('get_tag', uuid=instance.id) }}"/>
<table>
{% if "name" not in disabled_columns %}
<tr {% if "name" in hidden_columns %}style="display: none;"{% endif %}><td>
<label for="input-tag-name">{{ _("name") }}</label>:
</td><td>
<input id="input-tag-name"
value="{{ instance.name }}"
type="text" name="name" autocomplete="off"
/>
</td></tr>
{% endif %}
</table>
<input type="submit">
</form>

View File

@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block content %}
<h2>{{ _("Create") }} {{_("Tag") }}</h2>
{% include "tag/_create.html" %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block content %}
<a href="{{ url_for('list_tag', uuid=instance.id) }}">{{ _("list") }}</a>
<h2>{% include "tag/_title.html" %}</h2>
<ul>
{% for blog_post in instance.blog_posts %}
<li><a href="{{ url_for('blog_post_filename', filename=blog_post.filename) }}">{{ blog_post.title }}</a> - {{ _("Created on") }} {{ blog_post.created_dt|to_dt|format_dt }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "layout.html" %}
{% block content %}
<h2>{{ _("Tags") }}</h2>
{% if current_user.is_authenticated %}
<a href="{{ url_for('create_tag') }}">{{ _("Create") }}</a>
{% endif %}
<br>
{% include "tag/_list.html" %}
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "layout.html" %}
{% block content %}
<h2>{{ _("Search results for") }} {{ _("Tags") }}</h2>
<a href="{{ url_for('create_tag') }}">{{ _("Create") }}</a>
<br>
{% include "tag/_search.html" %}
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "layout.html" %}
{% block content %}
<h2>{{ _("Tags") }}</h2>
<a href="{{ url_for('create_tag') }}">{{ _("Create") }}</a>
<br>
{% include "tag/_table.html" %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "layout.html" %}
{% block content %}
<h2>{{ _("Edit") }} {% include "tag/_title.html" %}</h2>
{% include "tag/_update.html" %}
{% endblock %}

View File

@ -21,6 +21,9 @@ columns:
- name: title
- name: body
type: long_text
- name: tag
type: relationship
multiple: yes
- name: created_dt
- name: updated_dt
display:

View File

@ -0,0 +1,20 @@
name: Tag
interits:
- Ownable
access:
- verb: all
login_required: true
roles_required:
- admin
- verb: get
login_required: false
- verb: list
login_required: false
- verb: table
login_required: false
- verb: search
login_required: false
columns:
- name: name
display:
primary: name