Compare commits

...

22 commits

Author SHA1 Message Date
Lance Edgar 212e6ffc92 docs: update project links, kallithea -> forgejo 2024-09-14 12:13:24 -05:00
Lance Edgar 65b9d059e8 docs: use markdown for readme file 2024-09-13 18:11:51 -05:00
Lance Edgar 5884901d75 bump: version 0.3.0 → 0.3.1 2024-07-01 14:02:13 -05:00
Lance Edgar 52a1d15f7a fix: remove incorrect entry points
missed these when copy-pasting apparently
2024-07-01 12:33:05 -05:00
Lance Edgar 945d595329 bump: version 0.2.0 → 0.3.0 2024-06-10 19:35:55 -05:00
Lance Edgar 29c3429d2e feat: switch from setup.cfg to pyproject.toml + hatchling 2024-06-10 19:35:42 -05:00
Lance Edgar 6eb483fe90 Fix default dist filename for release task
not sure why this fix was needed, did setuptools behavior change?
2024-06-06 18:26:47 -05:00
Lance Edgar 3ea20c2d8c Update changelog 2024-06-06 18:25:57 -05:00
Lance Edgar eb904aea3b Refactor per rename of Harvest cache models 2023-10-04 15:56:02 -05:00
Lance Edgar f388f2d6cf Add button to re-import Harvest cache time entry from API 2023-10-04 13:10:28 -05:00
Lance Edgar a1c9199416 Replace setup.py contents with setup.cfg 2023-05-16 13:06:47 -05:00
Lance Edgar fc70b3c8ae Add xref buttons for all Harvest views 2023-03-25 13:21:02 -05:00
Lance Edgar 4c64dbc536 Refactor Query.get() => Session.get() per SQLAlchemy 1.4 2023-02-11 22:09:24 -06:00
Lance Edgar 15ea726b9b Make all Harvest models touchable 2023-01-18 13:11:26 -06:00
Lance Edgar 8da3f89524 Misc. tweaks to improve viewing Harvest cache records 2022-03-09 19:43:18 -06:00
Lance Edgar f3e05124c3 Use new config syntax for all Harvest views 2022-03-03 19:33:01 -06:00
Lance Edgar cc48cf5013 Improve filter sequence for Harvest Projects grid 2022-02-11 21:07:58 -06:00
Lance Edgar d35baf15f6 Add xref helper utils template 2022-02-11 19:16:47 -06:00
Lance Edgar 4bad4e262c Be smarter about showing non-active Harvest Clients 2022-02-10 10:22:56 -06:00
Lance Edgar a270d1dcc2 Expose HarvestUser.person for editing 2022-01-30 17:41:17 -06:00
Lance Edgar b8fd9c3022 Allow editing of Harvest cache data
i needed it to fix some data sync..
2022-01-30 12:38:15 -06:00
Lance Edgar 14e021db0d Cleanup views for Harvest Projects 2022-01-29 19:36:17 -06:00
18 changed files with 560 additions and 165 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
*~
*.pyc
dist/
tailbone_harvest.egg-info/

View file

@ -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.

11
README.md Normal file
View file

@ -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.

View file

@ -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/

42
pyproject.toml Normal file
View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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,
)

View file

@ -1,3 +1,6 @@
# -*- coding: utf-8; -*-
__version__ = '0.1.0'
from importlib.metadata import version
__version__ = version('tailbone-harvest')

View file

@ -0,0 +1,23 @@
## -*- coding: utf-8; -*-
<%def name="render_xref_buttons()">
<b-button type="is-primary"
% if harvest_url:
tag="a" href="${harvest_url}" target="_blank"
% else:
disabled title="${harvest_why_no_url}"
% endif
icon-pack="fas"
icon-left="external-link-alt">
View in Harvest
</b-button>
</%def>
<%def name="render_xref_helper()">
<nav class="panel">
<p class="panel-heading">Cross-Reference</p>
<div class="panel-block buttons">
${self.render_xref_buttons()}
</div>
</nav>
</%def>

View file

@ -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'):
<nav class="panel">
<p class="panel-heading">Re-Import</p>
<div class="panel-block buttons">
<div style="display: flex; flex-direction: column;">
% if master.has_perm('import_from_harvest'):
${h.form(master.get_action_url('import_from_harvest', instance), **{'@submit': 'importFromHarvestSubmitting = true'})}
${h.csrf_token(request)}
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="redo"
:disabled="importFromHarvestSubmitting">
{{ importFromHarvestSubmitting ? "Working, please wait..." : "Re-Import from Harvest" }}
</b-button>
${h.end_form()}
% endif
</div>
</div>
</nav>
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
% if master.has_perm('import_from_harvest'):
<script type="text/javascript">
ThisPageData.importFromHarvestSubmitting = false
</script>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,16 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" />
<%def name="page_content()">
% if instance.avatar_url:
<div style="margin: 1rem;">
<img src="${instance.avatar_url}" />
</div>
% endif
${parent.page_content()}
</%def>
${parent.body()}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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}')