diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index d35b8a35..2267b8dc 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -615,6 +615,9 @@ class Form(object): elif type_ == 'text': self.set_renderer(key, self.render_pre_sans_serif) self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8)) + elif type_ == 'text_wrapped': + self.set_renderer(key, self.render_pre_sans_serif_wrapped) + self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8)) elif type_ == 'file': tmpstore = SessionFileUploadTempStore(self.request) kw = {'widget': dfwidget.FileUploadWidget(tmpstore), @@ -914,14 +917,26 @@ class Form(object): return "" return HTML.tag('pre', value) - def render_pre_sans_serif(self, record, field_name): + def render_pre_sans_serif(self, record, field_name, wrapped=False): value = self.obtain_value(record, field_name) if value is None: return "" - # this uses a Bulma helper class, for which we also add custom styles - # to our "default" base.css (for jquery theme) - return HTML.tag('pre', class_='is-family-sans-serif', - c=value) + + kwargs = { + 'c': value, + # this uses a Bulma helper class, for which we also add + # custom styles to our "default" base.css (for jquery + # theme) + 'class_': 'is-family-sans-serif', + } + + if wrapped: + kwargs['style'] = 'white-space: pre-wrap;' + + return HTML.tag('pre', **kwargs) + + def render_pre_sans_serif_wrapped(self, record, field_name): + return self.render_pre_sans_serif(record, field_name, wrapped=True) def obtain_value(self, record, field_name): if record: diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 1e6787fa..f918fad4 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1418,12 +1418,13 @@ class GridAction(object): """ def __init__(self, key, label=None, url='#', icon=None, target=None, - click_handler=None): + link_class=None, click_handler=None): self.key = key self.label = label or prettify(key) self.icon = icon self.url = url self.target = target + self.link_class = link_class self.click_handler = click_handler def get_url(self, row, i): diff --git a/tailbone/templates/customers/view.mako b/tailbone/templates/customers/view.mako index 6c9de1ce..81e05aaa 100644 --- a/tailbone/templates/customers/view.mako +++ b/tailbone/templates/customers/view.mako @@ -26,4 +26,29 @@ % endif %def> +<%def name="render_buefy_form()"> +
There are no users assigned to this role.
% endif + % endif %def> +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + +%def> ${parent.body()} diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index 723beb5a..7132a767 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -173,6 +173,7 @@ class CustomerView(MasterView): super(CustomerView, self).configure_common_form(f) customer = f.model_instance permission_prefix = self.get_permission_prefix() + use_buefy = self.get_use_buefy() f.set_renderer('default_email', self.render_default_email) if not self.creating and customer.emails: @@ -217,13 +218,15 @@ class CustomerView(MasterView): f.set_renderer('person', self.form_render_person) # people - if self.creating: - f.remove_field('people') - elif self.viewing and self.request.has_perm('{}.detach_person'.format(permission_prefix)): - f.set_renderer('people', self.render_people_removable) + if self.viewing: + if use_buefy: + f.set_renderer('people', self.render_people_buefy) + elif self.has_perm('detach_person'): + f.set_renderer('people', self.render_people_removable) + else: + f.set_renderer('people', self.render_people) else: - f.set_renderer('people', self.render_people) - f.set_readonly('people') + f.remove('people') # groups if self.creating: @@ -245,7 +248,30 @@ class CustomerView(MasterView): f.set_readonly('members') def template_kwargs_view(self, **kwargs): + kwargs = super(CustomerView, self).template_kwargs_view(**kwargs) + kwargs['show_profiles_helper'] = self.show_profiles_helper + + use_buefy = self.get_use_buefy() + if use_buefy: + customer = kwargs['instance'] + people = [] + for person in customer.people: + people.append({ + 'uuid': person.uuid, + 'full_name': person.display_name, + 'first_name': person.first_name, + 'last_name': person.last_name, + '_action_url_view': self.request.route_url('people.view', + uuid=person.uuid), + '_action_url_edit': self.request.route_url('people.edit', + uuid=person.uuid), + '_action_url_detach': self.request.route_url('customers.detach_person', + uuid=customer.uuid, + person_uuid=person.uuid), + }) + kwargs['people_data'] = people + return kwargs def unique_id(self, node, value): @@ -318,6 +344,35 @@ class CustomerView(MasterView): main_actions=actions) return HTML.literal(g.render_grid()) + def render_people_buefy(self, customer, field): + route_prefix = self.get_route_prefix() + permission_prefix = self.get_permission_prefix() + + factory = self.get_grid_factory() + g = factory( + key='{}.people'.format(route_prefix), + data=[], + columns=[ + 'full_name', + 'first_name', + 'last_name', + ], + sortable=True, + sorters={'full_name': True, 'first_name': True, 'last_name': True}, + ) + + if self.request.has_perm('people.view'): + g.main_actions.append(self.make_action('view', icon='eye')) + if self.request.has_perm('people.edit'): + g.main_actions.append(self.make_action('edit', icon='edit')) + if self.has_perm('detach_person'): + g.main_actions.append(self.make_action('detach', icon='minus-circle', + link_class='has-text-warning', + click_handler="$emit('detach-person', props.row._action_url_detach)")) + + return HTML.literal( + g.render_buefy_table_element(data_prop='peopleData')) + def render_groups(self, customer, field): groups = customer.groups if not groups: @@ -372,18 +427,25 @@ class CustomerView(MasterView): def _customer_defaults(cls, config): route_prefix = cls.get_route_prefix() url_prefix = cls.get_url_prefix() + instance_url_prefix = cls.get_instance_url_prefix() permission_prefix = cls.get_permission_prefix() model_key = cls.get_model_key() model_title = cls.get_model_title() # detach person if cls.people_detachable: - config.add_tailbone_permission(permission_prefix, '{}.detach_person'.format(permission_prefix), + config.add_tailbone_permission(permission_prefix, + '{}.detach_person'.format(permission_prefix), "Detach a Person from a {}".format(model_title)) - config.add_route('{}.detach_person'.format(route_prefix), '{}/{{{}}}/detach-person/{{person_uuid}}'.format(url_prefix, model_key), + # TODO: this should require POST, but we'll add that once + # we can assume a Buefy theme is present, to avoid having + # to implement the logic in old jquery... + config.add_route('{}.detach_person'.format(route_prefix), + '{}/detach-person/{{person_uuid}}'.format(instance_url_prefix), # request_method='POST', ) - config.add_view(cls, attr='detach_person', route_name='{}.detach_person'.format(route_prefix), + config.add_view(cls, attr='detach_person', + route_name='{}.detach_person'.format(route_prefix), permission='{}.detach_person'.format(permission_prefix)) # TODO: deprecate / remove this diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 8efab65c..9ae3e19e 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -1152,8 +1152,88 @@ class ProductView(MasterView): kwargs['costs_label_vendor'] = "Vendor" kwargs['costs_label_code'] = "Order Code" kwargs['costs_label_case_size'] = "Case Size" + + if use_buefy: + kwargs['vendor_sources'] = self.get_context_vendor_sources(product) + kwargs['lookup_codes'] = self.get_context_lookup_codes(product) + return kwargs + def get_context_vendor_sources(self, product): + app = self.get_rattail_app() + route_prefix = self.get_route_prefix() + + factory = self.get_grid_factory() + g = factory( + key='{}.vendor_sources'.format(route_prefix), + data=[], + columns=[ + 'preferred', + 'vendor', + 'vendor_item_code', + 'case_size', + 'case_cost', + 'unit_cost', + 'status', + ], + labels={ + 'preferred': "Pref.", + 'vendor_item_code': "Order Code", + }, + ) + + sources = [] + link_vendor = self.request.has_perm('vendors.view') + for cost in product.costs: + + source = { + 'uuid': cost.uuid, + 'preferred': "X" if cost.preference == 1 else None, + 'vendor_item_code': cost.code, + 'case_size': app.render_quantity(cost.case_size), + 'case_cost': app.render_currency(cost.case_cost), + 'unit_cost': app.render_currency(cost.unit_cost, scale=4), + 'status': "discontinued" if cost.discontinued else "available", + } + + text = six.text_type(cost.vendor) + if link_vendor: + url = self.request.route_url('vendors.view', uuid=cost.vendor.uuid) + source['vendor'] = tags.link_to(text, url) + else: + source['vendor'] = text + + sources.append(source) + + return {'grid': g, 'data': sources} + + def get_context_lookup_codes(self, product): + route_prefix = self.get_route_prefix() + + factory = self.get_grid_factory() + g = factory( + key='{}.lookup_codes'.format(route_prefix), + data=[], + columns=[ + 'sequence', + 'code', + ], + labels={ + 'sequence': "Seq.", + }, + ) + + lookup_codes = [] + for code in product._codes: + + lookup_codes.append({ + 'uuid': code.uuid, + 'sequence': code.ordinal, + 'code': code.code, + }) + + return {'grid': g, 'data': lookup_codes} + def get_regular_price_history(self, product): """ Returns a sequence of "records" which corresponds to the given diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index 9b44dcdf..3cd62571 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -63,6 +63,7 @@ class RoleView(PrincipalMasterView): 'name', 'session_timeout', 'notes', + 'users', 'permissions', ] @@ -147,7 +148,13 @@ class RoleView(PrincipalMasterView): f.set_validator('name', self.unique_name) # notes - f.set_type('notes', 'text') + f.set_type('notes', 'text_wrapped') + + # users + if use_buefy and self.viewing or self.deleting: + f.set_renderer('users', self.render_users) + else: + f.remove('users') # permissions self.tailbone_permissions = self.get_available_permissions() @@ -171,6 +178,40 @@ class RoleView(PrincipalMasterView): if self.editing and role is guest_role(self.Session()): f.set_readonly('session_timeout') + def render_users(self, role, field): + + if role is guest_role(self.Session()): + return ("The guest role is implied for all anonymous users, " + "i.e. when not logged in.") + + if role is authenticated_role(self.Session()): + return ("The authenticated role is implied for all users, " + "but only when logged in.") + + route_prefix = self.get_route_prefix() + permission_prefix = self.get_permission_prefix() + factory = self.get_grid_factory() + g = factory( + key='{}.users'.format(route_prefix), + data=[], + columns=[ + 'full_name', + 'username', + 'active', + ], + sortable=True, + sorters={'full_name': True, 'username': True, 'active': True}, + default_sortkey='full_name', + ) + + if self.request.has_perm('users.view'): + g.main_actions.append(self.make_action('view', icon='eye')) + if self.request.has_perm('users.edit'): + g.main_actions.append(self.make_action('edit', icon='edit')) + + return HTML.literal( + g.render_buefy_table_element(data_prop='usersData')) + def get_available_permissions(self): """ Should return a dictionary with all "available" permissions. The @@ -259,8 +300,28 @@ class RoleView(PrincipalMasterView): main_actions=actions) else: kwargs['users'] = None + kwargs['guest_role'] = guest_role(self.Session()) kwargs['authenticated_role'] = authenticated_role(self.Session()) + + use_buefy = self.get_use_buefy() + if use_buefy: + role = kwargs['instance'] + if role not in (kwargs['guest_role'], kwargs['authenticated_role']): + users_data = [] + for user in role.users: + users_data.append({ + 'uuid': user.uuid, + 'full_name': user.display_name, + 'username': user.username, + 'active': "Yes" if user.active else "No", + '_action_url_view': self.request.route_url('users.view', + uuid=user.uuid), + '_action_url_edit': self.request.route_url('users.edit', + uuid=user.uuid), + }) + kwargs['users_data'] = users_data + return kwargs def before_delete(self, role):