Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ Note that `.` becomes `_` and the config paths is joined by underscores.

## Important Settings

### E-Signature Service Configuration (Switching from SignNow to OpenSign)
Ubersystem supports both **SignNow** (commercial SaaS) and **OpenSign Labs** (open-source cloud / self-hosted Docker) for automated electronic signature collection on Dealer Terms & Conditions (`terms_and_conditions`).

By default, when a dealer is accepted into the marketplace, Ubersystem checks `signnow_dealer_template_id`. However, via the `BaseSignatureRequest` and `get_esign_request()` architecture, Ubersystem dynamically routes all document workflows to **OpenSign** whenever an OpenSign template ID is provided.

#### How to Switch to OpenSign
To switch your event from SignNow to OpenSign, configure the following settings in your `config.ini` (`uber.ini`):

```ini
# 1. Provide the OpenSign Template ID (Setting this overrides SignNow automatically)
opensign_dealer_template_id = "your_opensign_template_id_here"
opensign_dealer_folder_id = "optional_opensign_folder_id"

# 2. Configure the API URL and Token
# By default, Ubersystem connects to OpenSign Cloud (https://api.opensignlabs.com/v1).
# If you are running a self-hosted OpenSign Docker container, override the API URL:
opensign_api_url = "https://signing.yourcon.org/v1"

# For local development or non-AWS deployments, provide your static API token directly:
opensign_api_token = "your_static_api_token_here"

# For AWS Production deployments, leave opensign_api_token blank and provide your AWS Secrets Manager secret name:
# Ubersystem will automatically pull, cache, and rotate the API token in Redis every 15 seconds.
aws_opensign_secret_name = "production/opensign/credentials"
```

Once `opensign_dealer_template_id` is set, all frontend templates (`/preregistration/group_members`, `/group_admin/form`, `/group_admin/dealers`), background polling cron tasks (`check_document_signed`), and PDF downloads (`download_esign_document`) will automatically communicate with OpenSign instead of SignNow.

## Generated Configuration
When working on Uber for a specific event, it's best to start with config overrides copied from that event. Some events have their config overrides available in their own repository. In these cases, you can run the `make_config.py` script in the root of this repo, which will download and compile the config you need into ready-made config files.

Expand Down
419 changes: 419 additions & 0 deletions tests/uber/test_open_sign.py

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions uber/automated_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,24 @@ def generic_placeholder(a): return a.placeholder and (not deferred_attendee_plac
when=days_before(7, c.PLACEHOLDER_DEADLINE if c.PLACEHOLDER_DEADLINE else c.UBER_TAKEDOWN),
ident='badge_confirmation_reminder_last_chance')

AutomatedEmailFixture(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Unexpected indentation

Attendee,
f'Claim your badge for {c.EVENT_NAME_AND_YEAR}!',
'placeholders/regular.txt',
lambda a: a.placeholder and a.registered_local > earliest_opening_date and a.paid == c.NEED_NOT_PAY,
'generic_badge_confirmation_comped',
sender=c.CONTACT_EMAIL,
allow_at_the_con=True)

AutomatedEmailFixture(
Attendee,
f'Please complete your {c.EVENT_NAME_AND_YEAR} registration',
'placeholders/regular.txt',
lambda a: a.placeholder and a.registered_local > earliest_opening_date and \
a.paid != c.NEED_NOT_PAY and 'converted badge' not in a.admin_notes.lower(),
'generic_badge_confirmation',
sender=c.CONTACT_EMAIL,
allow_at_the_con=True) (Implement provider-agnostic OpenSign e-signature integration with comprehensive test suite and documentation)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ), found with

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected ,, found )


Comment on lines +703 to 704

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected :, found newline

if c.VOLUNTEER_CHECKLIST_OPEN:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ruff] reported by reviewdog 🐶
Expected a statement

StopsEmailFixture(
Expand Down
20 changes: 19 additions & 1 deletion uber/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,14 @@ class Config(_Overridable):
For all of the datetime config options, we also define BEFORE_ and AFTER_ properties, e.g. you can
check the booleans returned by c.BEFORE_PLACEHOLDER_DEADLINE or c.AFTER_PLACEHOLDER_DEADLINE
"""
@property
def ESIGN_DEALER_TEMPLATE_ID(self):
return getattr(self, 'SIGNNOW_DEALER_TEMPLATE_ID', '') or getattr(self, 'OPENSIGN_DEALER_TEMPLATE_ID', '')

@property
def ESIGN_DEALER_FOLDER_ID(self):
return getattr(self, 'SIGNNOW_DEALER_FOLDER_ID', '') or getattr(self, 'OPENSIGN_DEALER_FOLDER_ID', '')

def get_oneday_price(self, dt):
return self.BADGE_PRICES['single_day'].get(dt.strftime('%A'), self.DEFAULT_SINGLE_DAY)

Expand Down Expand Up @@ -1585,6 +1593,7 @@ def get_secret(self, secret_name):

def get_all_secrets(self):
self.get_signnow_secret()
self.get_opensign_secret()

def get_signnow_secret(self):
if not c.AWS_SIGNNOW_SECRET_NAME:
Expand All @@ -1596,6 +1605,15 @@ def get_signnow_secret(self):
c.SIGNNOW_CLIENT_SECRET = signnow_secret.get('CLIENT_SECRET', '') or c.SIGNNOW_CLIENT_SECRET
return signnow_secret

def get_opensign_secret(self):
if not c.AWS_OPENSIGN_SECRET_NAME:
return

opensign_secret = self.get_secret(c.AWS_OPENSIGN_SECRET_NAME)
if opensign_secret:
c.OPENSIGN_API_TOKEN = opensign_secret.get('API_TOKEN', '') or c.OPENSIGN_API_TOKEN
return opensign_secret

def get_config_files(plugin_name, module_dir):
config_files_str = os.environ.get(f"{plugin_name.upper()}_CONFIG_FILES", "")
absolute_config_files = []
Expand Down Expand Up @@ -1727,7 +1745,7 @@ def build_hotel_inventory(inventory_type, room_types):


c = Config()
_config = parse_config("uber", pathlib.Path("/app/uber")) # outside this module, we use the above c global instead of using this directly
_config = parse_config("uber", pathlib.Path(__file__).resolve().parent) # outside this module, we use the above c global instead of using this directly
db_connection_string = os.environ.get('DB_CONNECTION_STRING')

for conf, val in _config['secret'].items():
Expand Down
7 changes: 7 additions & 0 deletions uber/configspec.ini
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ tools.cpstats.on = boolean(default=False)
aws_secret_service_name = string(default="")

aws_signnow_secret_name = string(default="")
aws_opensign_secret_name = string(default="")
aws_auth0_secret_name = string(default="")

# Configuration for AWS email sending and secrets fetching feature
Expand Down Expand Up @@ -953,6 +954,12 @@ signnow_access_token = string(default='')
signnow_username = string(default='')
signnow_password = string(default='')

# Configuration options for OpenSign e-signature integration (open-source alternative to SignNow)
opensign_api_url = string(default='https://api.opensignlabs.com/v1')
opensign_api_token = string(default='')
opensign_dealer_template_id = string(default='')
opensign_dealer_folder_id = string(default='')

# Used to connect to our celery task queue broker, for example RabbitMQ
broker_url = string(default="amqp://localhost//")

Expand Down
2 changes: 1 addition & 1 deletion uber/models/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,7 +1584,7 @@ def watchlist_guess(self):
watchentries = session.guess_attendee_watchentry(self)
return [w.to_dict() for w in watchentries]
except Exception as ex:
log.warning('Error guessing watchlist entry: {}', ex)
log.warning('Error guessing watchlist entry: %s', ex)
return None

@property
Expand Down
9 changes: 6 additions & 3 deletions uber/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ def cost_cents(self):
return self.cost * 100

@property
def signnow_texts_list(self):
def esign_texts_list(self):
"""
Returns a list of JSON representing uneditable texts fields to use for this group's document in SignNow.
Returns a list of JSON representing uneditable texts fields to use for this group's document in an e-signature provider.
"""
page_number = 2
textFont = 'Arial'
Expand All @@ -144,10 +144,13 @@ def signnow_texts_list(self):

return texts

signnow_texts_list = esign_texts_list

@property
def signnow_document_signed(self):
def esign_document_signed(self):
return self.terms_conditions_doc and self.terms_conditions_doc.signed

signnow_document_signed = esign_document_signed
def convert_to_shared(self, session):
self.tables = 0
if len(self.floating) < abs(1 - self.badges):
Expand Down
Loading
Loading