diff --git a/.gitignore b/.gitignore index 6ee5074..40ca5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +*~ +*.pyc +dist/ tailbone_harvest.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaaf9a..0728aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to tailbone-harvest will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.1 (2024-07-01) + +### Fix + +- remove incorrect entry points + +## v0.3.0 (2024-06-10) + +### Feat + +- switch from setup.cfg to pyproject.toml + hatchling + +## [0.2.0] - 2024-06-06 +Catch-up release. +### Changed +- lots of things... + ## [0.1.0] - 2022-01-29 ### Added - Initial version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec86877 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ + +# tailbone-harvest + +Rattail is a retail software framework, released under the GNU General +Public License. + +This package contains software interfaces for +[Harvest](https://www.getharvest.com/). + +Please see the [Rattail Project](https://rattailproject.org/) for more +information. diff --git a/README.rst b/README.rst deleted file mode 100644 index 4866f29..0000000 --- a/README.rst +++ /dev/null @@ -1,14 +0,0 @@ - -tailbone-harvest -================ - -Rattail is a retail software framework, released under the GNU General -Public License. - -This package contains software interfaces for `Harvest`_. - -.. _`Harvest`: https://www.getharvest.com/ - -Please see the `Rattail Project`_ for more information. - -.. _`Rattail Project`: https://rattailproject.org/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e9ffcd3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + +[project] +name = "tailbone-harvest" +version = "0.3.1" +description = "Tailbone integration package for Harvest" +readme = "README.md" +authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] +license = {text = "GNU GPL v3+"} +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Office/Business", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "invoke", + "rattail-harvest", + "Tailbone", +] + + +[project.urls] +Homepage = "https://rattailproject.org" +Repository = "https://forgejo.wuttaproject.org/rattail/tailbone-harvest" +Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone-harvest/src/branch/master/CHANGELOG.md" + + +[tool.commitizen] +version_provider = "pep621" +tag_format = "v$version" +update_changelog_on_bump = true diff --git a/setup.py b/setup.py deleted file mode 100644 index d333d3f..0000000 --- a/setup.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# Rattail. If not, see . -# -################################################################################ -""" -tailbone-harvest setup script -""" - -import os -from setuptools import setup, find_packages - - -here = os.path.abspath(os.path.dirname(__file__)) -exec(open(os.path.join(here, 'tailbone_harvest', '_version.py')).read()) -README = open(os.path.join(here, 'README.rst')).read() - - -requires = [ - # - # Version numbers within comments below have specific meanings. - # Basically the 'low' value is a "soft low," and 'high' a "soft high." - # In other words: - # - # If either a 'low' or 'high' value exists, the primary point to be - # made about the value is that it represents the most current (stable) - # version available for the package (assuming typical public access - # methods) whenever this project was started and/or documented. - # Therefore: - # - # If a 'low' version is present, you should know that attempts to use - # versions of the package significantly older than the 'low' version - # may not yield happy results. (A "hard" high limit may or may not be - # indicated by a true version requirement.) - # - # Similarly, if a 'high' version is present, and especially if this - # project has laid dormant for a while, you may need to refactor a bit - # when attempting to support a more recent version of the package. (A - # "hard" low limit should be indicated by a true version requirement - # when a 'high' version is present.) - # - # In any case, developers and other users are encouraged to play - # outside the lines with regard to these soft limits. If bugs are - # encountered then they should be filed as such. - # - # package # low high - - 'invoke', # 1.5.0 - 'rattail-harvest', # 0.1.0 - 'Tailbone', # 0.8.199 -] - - -setup( - name = "tailbone-harvest", - version = __version__, - author = "Lance Edgar", - author_email = "lance@edbob.org", - url = "https://rattailproject.org/", - description = "Tailbone integration package for Harvest", - long_description = README, - - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Office/Business', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], - - install_requires = requires, - packages = find_packages(), - include_package_data = True, -) diff --git a/tailbone_harvest/_version.py b/tailbone_harvest/_version.py index e41b669..0e7a01b 100644 --- a/tailbone_harvest/_version.py +++ b/tailbone_harvest/_version.py @@ -1,3 +1,6 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.0' +from importlib.metadata import version + + +__version__ = version('tailbone-harvest') diff --git a/tailbone_harvest/templates/.keepme b/tailbone_harvest/templates/.keepme deleted file mode 100644 index e69de29..0000000 diff --git a/tailbone_harvest/templates/harvest-util.mako b/tailbone_harvest/templates/harvest-util.mako new file mode 100644 index 0000000..907cc96 --- /dev/null +++ b/tailbone_harvest/templates/harvest-util.mako @@ -0,0 +1,23 @@ +## -*- coding: utf-8; -*- + +<%def name="render_xref_buttons()"> + + View in Harvest + + + +<%def name="render_xref_helper()"> + + diff --git a/tailbone_harvest/templates/harvest/time-entries/view.mako b/tailbone_harvest/templates/harvest/time-entries/view.mako new file mode 100644 index 0000000..3b36ff5 --- /dev/null +++ b/tailbone_harvest/templates/harvest/time-entries/view.mako @@ -0,0 +1,45 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="object_helpers()"> + ${parent.object_helpers()} + ${self.render_import_helper()} + + +<%def name="render_import_helper()"> + % if master.has_perm('import_from_harvest'): + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + % if master.has_perm('import_from_harvest'): + + % endif + + + +${parent.body()} diff --git a/tailbone_harvest/templates/harvest/users/view.mako b/tailbone_harvest/templates/harvest/users/view.mako new file mode 100644 index 0000000..245deff --- /dev/null +++ b/tailbone_harvest/templates/harvest/users/view.mako @@ -0,0 +1,16 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="page_content()"> + + % if instance.avatar_url: +
+ +
+ % endif + + ${parent.page_content()} + + + +${parent.body()} diff --git a/tailbone_harvest/views/harvest/clients.py b/tailbone_harvest/views/harvest/clients.py index 1f7be2b..d80751d 100644 --- a/tailbone_harvest/views/harvest/clients.py +++ b/tailbone_harvest/views/harvest/clients.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,18 +24,19 @@ Harvest Client views """ -from rattail_harvest.db.model import HarvestClient +from rattail_harvest.db.model import HarvestCacheClient +from rattail_harvest.harvest.config import get_harvest_url from webhelpers2.html import HTML, tags from .master import HarvestMasterView -class HarvestClientView(HarvestMasterView): +class HarvestCacheClientView(HarvestMasterView): """ Master view for Harvest Clients """ - model_class = HarvestClient + model_class = HarvestCacheClient url_prefix = '/harvest/clients' route_prefix = 'harvest.clients' @@ -46,18 +47,25 @@ class HarvestClientView(HarvestMasterView): ] def configure_grid(self, g): - super(HarvestClientView, self).configure_grid(g) + super().configure_grid(g) g.filters['name'].default_active = True g.filters['name'].default_verb = 'contains' + g.filters['is_active'].default_active = True + g.filters['is_active'].default_verb = 'is_true' + g.set_sort_defaults('name') g.set_link('id') g.set_link('name') + def grid_extra_class(self, client, i): + if not client.is_active: + return 'warning' + def configure_form(self, f): - super(HarvestClientView, self).configure_form(f) + super().configure_form(f) # projects f.set_renderer('projects', self.render_projects) @@ -77,6 +85,26 @@ class HarvestClientView(HarvestMasterView): items.append(HTML.tag('li', c=[tags.link_to(text, url)])) return HTML.tag('ul', c=items) + def get_xref_buttons(self, client): + buttons = super().get_xref_buttons(client) + model = self.model + + # harvest proper + url = get_harvest_url(self.rattail_config) + if url: + url = '{}/clients'.format(url) + buttons.append(self.make_xref_button(url=url, + text="View in Harvest")) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + HarvestCacheClientView = kwargs.get('HarvestCacheClientView', base['HarvestCacheClientView']) + HarvestCacheClientView.defaults(config) + def includeme(config): - HarvestClientView.defaults(config) + defaults(config) diff --git a/tailbone_harvest/views/harvest/master.py b/tailbone_harvest/views/harvest/master.py index 5d206ac..5230987 100644 --- a/tailbone_harvest/views/harvest/master.py +++ b/tailbone_harvest/views/harvest/master.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,6 +24,10 @@ Harvest master view """ +from rattail_harvest.db.model import HarvestCacheTimeEntry + +from webhelpers2.html import tags + from tailbone.views import MasterView @@ -32,10 +36,58 @@ class HarvestMasterView(MasterView): Base class for Harvest master views """ creatable = False - editable = False - deletable = False + touchable = True has_versions = True + model_row_class = HarvestCacheTimeEntry labels = { 'id': "ID", + 'user_id': "User ID", + 'client_id': "Client ID", + 'project_id': "Project ID", + 'task_id': "Task ID", + 'invoice_id': "Invoice ID", } + + row_labels = { + 'id': "ID", + } + + def configure_form(self, f): + super(HarvestMasterView, self).configure_form(f) + f.remove('time_entries') + + def render_harvest_user(self, obj, field): + user = getattr(obj, field) + if user: + text = str(user) + url = self.request.route_url('harvest.users.view', uuid=user.uuid) + return tags.link_to(text, url) + + def render_harvest_client(self, obj, field): + client = getattr(obj, field) + if client: + text = str(client) + url = self.request.route_url('harvest.clients.view', uuid=client.uuid) + return tags.link_to(text, url) + + def render_harvest_project(self, obj, field): + project = getattr(obj, field) + if project: + text = str(project) + url = self.request.route_url('harvest.projects.view', uuid=project.uuid) + return tags.link_to(text, url) + + def render_harvest_task(self, obj, field): + task = getattr(obj, field) + if task: + text = str(task) + url = self.request.route_url('harvest.tasks.view', uuid=task.uuid) + return tags.link_to(text, url) + + def configure_row_grid(self, g): + super(HarvestMasterView, self).configure_row_grid(g) + g.set_sort_defaults('spent_date', 'desc') + + def row_view_action_url(self, entry, i): + return self.request.route_url('harvest.time_entries.view', uuid=entry.uuid) diff --git a/tailbone_harvest/views/harvest/projects.py b/tailbone_harvest/views/harvest/projects.py index 30b8a9d..55bf236 100644 --- a/tailbone_harvest/views/harvest/projects.py +++ b/tailbone_harvest/views/harvest/projects.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,19 +24,22 @@ Harvest Project views """ -from rattail_harvest.db.model import HarvestProject +from rattail_harvest.db.model import HarvestCacheProject +from rattail_harvest.harvest.config import get_harvest_url from .master import HarvestMasterView -class HarvestProjectView(HarvestMasterView): +class HarvestCacheProjectView(HarvestMasterView): """ Master view for Harvest Projects """ - model_class = HarvestProject + model_class = HarvestCacheProject url_prefix = '/harvest/projects' route_prefix = 'harvest.projects' + has_rows = True + grid_columns = [ 'id', 'client', @@ -49,26 +52,89 @@ class HarvestProjectView(HarvestMasterView): 'fee', ] + row_grid_columns = [ + 'id', + 'spent_date', + 'user', + 'client', + 'task', + 'hours', + ] + def configure_grid(self, g): - super(HarvestProjectView, self).configure_grid(g) + super().configure_grid(g) model = self.model - g.set_joiner('client', lambda q: q.outerjoin(model.HarvestClient)) - g.set_sorter('client', model.HarvestClient.name) - g.set_filter('client', model.HarvestClient.name, label="Client Name") + g.set_joiner('client', lambda q: q.outerjoin(model.HarvestCacheClient)) + g.set_sorter('client', model.HarvestCacheClient.name) + g.set_filter('client', model.HarvestCacheClient.name, label="Client Name") g.filters['client'].default_active = True g.filters['client'].default_verb = 'contains' + g.filters['is_active'].default_active = True + g.filters['is_active'].default_verb = 'is_true' + g.set_type('hourly_rate', 'currency') g.set_type('fee', 'currency') g.set_sort_defaults('client') + g.set_filters_sequence([ + 'id', + 'name', + 'client', + ]) + g.set_link('id') g.set_link('client') g.set_link('name') g.set_link('code') + def grid_extra_class(self, project, i): + if not project.is_active: + return 'warning' + + def configure_form(self, f): + super().configure_form(f) + + f.set_type('hourly_rate', 'currency') + + if self.editing: + f.remove('client') + f.set_type('over_budget_notification_date', 'date_jquery') + f.set_type('starts_on', 'date_jquery') + f.set_type('ends_on', 'date_jquery') + f.set_readonly('created_at') + f.set_readonly('updated_at') + + def get_xref_buttons(self, project): + buttons = super().get_xref_buttons(project) + model = self.model + + # harvest + url = get_harvest_url(self.rattail_config) + if url: + url = '{}/projects/{}'.format(url, project.id) + buttons.append(self.make_xref_button(url=url, + text="View in Harvest")) + + return buttons + + def get_row_data(self, project): + model = self.model + return self.Session.query(model.HarvestCacheTimeEntry)\ + .filter(model.HarvestCacheTimeEntry.project == project) + + def get_parent(self, entry): + return entry.project + + +def defaults(config, **kwargs): + base = globals() + + HarvestCacheProjectView = kwargs.get('HarvestCacheProjectView', base['HarvestCacheProjectView']) + HarvestCacheProjectView.defaults(config) + def includeme(config): - HarvestProjectView.defaults(config) + defaults(config) diff --git a/tailbone_harvest/views/harvest/tasks.py b/tailbone_harvest/views/harvest/tasks.py index a3b61b4..2cf52df 100644 --- a/tailbone_harvest/views/harvest/tasks.py +++ b/tailbone_harvest/views/harvest/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,16 +24,17 @@ Harvest Task views """ -from rattail_harvest.db.model import HarvestTask +from rattail_harvest.db.model import HarvestCacheTask +from rattail_harvest.harvest.config import get_harvest_url from .master import HarvestMasterView -class HarvestTaskView(HarvestMasterView): +class HarvestCacheTaskView(HarvestMasterView): """ Master view for Harvest Tasks """ - model_class = HarvestTask + model_class = HarvestCacheTask url_prefix = '/harvest/tasks' route_prefix = 'harvest.tasks' @@ -47,7 +48,7 @@ class HarvestTaskView(HarvestMasterView): ] def configure_grid(self, g): - super(HarvestTaskView, self).configure_grid(g) + super().configure_grid(g) g.set_sort_defaults('name') @@ -55,11 +56,31 @@ class HarvestTaskView(HarvestMasterView): g.set_link('name') def configure_form(self, f): - super(HarvestTaskView, self).configure_form(f) + super().configure_form(f) # time_entries f.remove_field('time_entries') + def get_xref_buttons(self, task): + buttons = super().get_xref_buttons(task) + model = self.model + + # harvest + url = get_harvest_url(self.rattail_config) + if url: + url = '{}/tasks'.format(url) + buttons.append(self.make_xref_button(url=url, + text="View in Harvest")) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + HarvestCacheTaskView = kwargs.get('HarvestCacheTaskView', base['HarvestCacheTaskView']) + HarvestCacheTaskView.defaults(config) + def includeme(config): - HarvestTaskView.defaults(config) + defaults(config) diff --git a/tailbone_harvest/views/harvest/time_entries.py b/tailbone_harvest/views/harvest/time_entries.py index 3ddfdab..fc04bf8 100644 --- a/tailbone_harvest/views/harvest/time_entries.py +++ b/tailbone_harvest/views/harvest/time_entries.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,27 +24,20 @@ Harvest Time Entry views """ -from rattail_harvest.db.model import HarvestTimeEntry +from rattail_harvest.db.model import HarvestCacheTimeEntry +from rattail_harvest.harvest.config import get_harvest_url from .master import HarvestMasterView -class HarvestTimeEntryView(HarvestMasterView): +class HarvestCacheTimeEntryView(HarvestMasterView): """ Master view for Harvest Time Entries """ - model_class = HarvestTimeEntry + model_class = HarvestCacheTimeEntry url_prefix = '/harvest/time-entries' route_prefix = 'harvest.time_entries' - labels = { - 'user_id': "User ID", - 'client_id': "Client ID", - 'project_id': "Project ID", - 'task_id': "Task ID", - 'invoice_id': "Invoice ID", - } - grid_columns = [ 'id', 'spent_date', @@ -57,7 +50,7 @@ class HarvestTimeEntryView(HarvestMasterView): ] def configure_grid(self, g): - super(HarvestTimeEntryView, self).configure_grid(g) + super().configure_grid(g) g.set_type('hours', 'duration_hours') @@ -68,6 +61,103 @@ class HarvestTimeEntryView(HarvestMasterView): g.set_link('client') g.set_link('notes') + def configure_form(self, f): + super().configure_form(f) + + # make sure id is first field + f.remove('id') + f.insert(0, 'id') + + # user + f.remove('user_id') + f.set_renderer('user', self.render_harvest_user) + + # client + f.remove('client_id') + f.set_renderer('client', self.render_harvest_client) + + # project + f.remove('project_id') + f.set_renderer('project', self.render_harvest_project) + + # task + f.remove('task_id') + f.set_renderer('task', self.render_harvest_task) + + # hours + f.set_renderer('hours', self.render_hours) + + f.set_type('notes', 'text') + + f.set_type('billable_rate', 'currency') + f.set_type('cost_rate', 'currency') + + def render_hours(self, entry, field): + hours = getattr(entry, field) + app = self.get_rattail_app() + duration = app.render_duration(hours=hours) + return f"{hours} ({duration})" + + def get_xref_buttons(self, entry): + buttons = super().get_xref_buttons(entry) + model = self.model + + # harvest + url = get_harvest_url(self.rattail_config) + if url: + url = '{}/time/day/{}/{}'.format( + url, + entry.spent_date.strftime('%Y/%m/%d'), + entry.user_id) + buttons.append(self.make_xref_button(url=url, + text="View in Harvest")) + + return buttons + + def import_from_harvest(self): + app = self.get_rattail_app() + handler = app.get_import_handler('to_rattail.from_harvest.import', require=True) + importer = handler.get_importer('HarvestCacheTimeEntry') + importer.session = self.Session() + importer.setup() + + cache_entry = self.get_instance() + if self.oneoff_import(importer, local_object=cache_entry): + self.request.session.flash(f"{self.get_model_title()} has been " + f"(re-)imported from Harvest: {cache_entry}") + else: + self.request.session.flash("Import failed!", 'error') + + return self.redirect(self.get_action_url('view', cache_entry)) + + @classmethod + def defaults(cls, config): + route_prefix = cls.get_route_prefix() + instance_url_prefix = cls.get_instance_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title = cls.get_model_title() + + # normal defaults + cls._defaults(config) + + # import from harvest + config.add_tailbone_permission(permission_prefix, + f'{permission_prefix}.import_from_harvest', + f"Re-Import {model_title} from Harvest") + config.add_route(f'{route_prefix}.import_from_harvest', + f'{instance_url_prefix}/import-from-harvest', + request_method='POST') + config.add_view(cls, attr='import_from_harvest', + route_name=f'{route_prefix}.import_from_harvest', + permission=f'{permission_prefix}.import_from_harvest') + + +def defaults(config, **kwargs): + base = globals() + + HarvestCacheTimeEntryView = kwargs.get('HarvestCacheTimeEntryView', base['HarvestCacheTimeEntryView']) + HarvestCacheTimeEntryView.defaults(config) + def includeme(config): - HarvestTimeEntryView.defaults(config) + defaults(config) diff --git a/tailbone_harvest/views/harvest/users.py b/tailbone_harvest/views/harvest/users.py index 8f87fb0..d360b83 100644 --- a/tailbone_harvest/views/harvest/users.py +++ b/tailbone_harvest/views/harvest/users.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,19 +24,27 @@ Harvest User views """ -from rattail_harvest.db.model import HarvestUser +from rattail_harvest.db.model import HarvestCacheUser +from rattail_harvest.harvest.config import get_harvest_url +import colander + +from tailbone import forms from .master import HarvestMasterView -class HarvestUserView(HarvestMasterView): +class HarvestCacheUserView(HarvestMasterView): """ Master view for Harvest Users """ - model_class = HarvestUser + model_class = HarvestCacheUser url_prefix = '/harvest/users' route_prefix = 'harvest.users' + labels = { + 'avatar_url': "Avatar URL", + } + grid_columns = [ 'id', 'first_name', @@ -48,7 +56,11 @@ class HarvestUserView(HarvestMasterView): ] def configure_grid(self, g): - super(HarvestUserView, self).configure_grid(g) + super().configure_grid(g) + model = self.model + + g.set_joiner('person_name', lambda q: q.outerjoin(model.Person)) + g.set_filter('person_name', model.Person.display_name) g.set_sort_defaults('first_name') @@ -58,11 +70,75 @@ class HarvestUserView(HarvestMasterView): g.set_link('email') def configure_form(self, f): - super(HarvestUserView, self).configure_form(f) + super().configure_form(f) + model = self.model + user = f.model_instance + # person + f.set_renderer('person', self.render_person) + if self.creating or self.editing: + if 'person' in f.fields: + f.remove('person_uuid') + f.replace('person', 'person_uuid') + person_display = "" + if self.request.method == 'POST': + if self.request.POST.get('person_uuid'): + person = self.Session.get(model.Person, + self.request.POST['person_uuid']) + if person: + person_display = str(person) + elif self.editing: + person_display = str(user.person or '') + people_url = self.request.route_url('people.autocomplete') + f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget( + field_display=person_display, service_url=people_url)) + f.set_validator('person_uuid', self.valid_person) + f.set_label('person_uuid', "Person") + else: + f.remove('person_uuid') + + f.set_type('weekly_capacity', 'duration') + + f.set_type('default_hourly_rate', 'currency') + f.set_type('cost_rate', 'currency') + + f.set_renderer('avatar_url', self.render_url) + + # timestamps + if self.creating or self.editing: + f.remove('created_at') + f.remove('updated_at') + + # time_entries # TODO: should add this as child rows/grid instead f.remove('time_entries') + def valid_person(self, node, value): + model = self.model + if value: + person = self.Session.get(model.Person, value) + if not person: + raise colander.Invalid(node, "Person not found (you must *select* a record)") + + def get_xref_buttons(self, user): + buttons = super().get_xref_buttons(user) + model = self.model + + # harvest proper + url = get_harvest_url(self.rattail_config) + if url: + url = '{}/team'.format(url) + buttons.append(self.make_xref_button(url=url, text="View in Harvest")) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + HarvestCacheUserView = kwargs.get('HarvestCacheUserView', base['HarvestCacheUserView']) + HarvestCacheUserView.defaults(config) + def includeme(config): - HarvestUserView.defaults(config) + defaults(config) diff --git a/tasks.py b/tasks.py index 88b264a..c4ed484 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -25,24 +25,36 @@ Tasks for tailbone-harvest """ import os +import re import shutil from invoke import task here = os.path.abspath(os.path.dirname(__file__)) -exec(open(os.path.join(here, 'tailbone_harvest', '_version.py')).read()) +__version__ = None +pattern = re.compile(r'^version = "(\d+\.\d+\.\d+)"$') +with open(os.path.join(here, 'pyproject.toml'), 'rt') as f: + for line in f: + line = line.rstrip('\n') + match = pattern.match(line) + if match: + __version__ = match.group(1) + break +if not __version__: + raise RuntimeError("could not parse version!") @task -def release(ctx): +def release(c): """ Release a new version of tailbone-harvest """ # rebuild local tar.gz file for distribution - shutil.rmtree('tailbone_harvest.egg-info') - ctx.run('python setup.py sdist --formats=gztar') + if os.path.exists('tailbone_harvest.egg-info'): + shutil.rmtree('tailbone_harvest.egg-info') + c.run('python -m build --sdist') # upload to public PyPI - filename = 'tailbone-harvest-{}.tar.gz'.format(__version__) - ctx.run('twine upload dist/{}'.format(filename)) + filename = f'tailbone_harvest-{__version__}.tar.gz' + c.run(f'twine upload dist/{filename}')