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>
+
+<%def name="render_xref_helper()">
+
+%def>
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>
+
+<%def name="render_import_helper()">
+ % if master.has_perm('import_from_harvest'):
+
+ % endif
+%def>
+
+<%def name="modify_this_page_vars()">
+ ${parent.modify_this_page_vars()}
+ % if master.has_perm('import_from_harvest'):
+
+ % endif
+%def>
+
+
+${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()}
+%def>
+
+
+${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}')