indico-plugin-stsa 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. indico_plugin_stsa-0.1.0/.github/scripts/check_wheel.py +85 -0
  2. indico_plugin_stsa-0.1.0/.gitignore +30 -0
  3. indico_plugin_stsa-0.1.0/LICENSE +32 -0
  4. indico_plugin_stsa-0.1.0/PKG-INFO +319 -0
  5. indico_plugin_stsa-0.1.0/README.md +297 -0
  6. indico_plugin_stsa-0.1.0/build-assets.py +96 -0
  7. indico_plugin_stsa-0.1.0/indico_stsa/__init__.py +22 -0
  8. indico_plugin_stsa-0.1.0/indico_stsa/blueprint.py +13 -0
  9. indico_plugin_stsa-0.1.0/indico_stsa/client/js/GroupLoginGate.jsx +101 -0
  10. indico_plugin_stsa-0.1.0/indico_stsa/client/js/MemberDiscountNotice.jsx +77 -0
  11. indico_plugin_stsa-0.1.0/indico_stsa/client/js/SignInButton.jsx +52 -0
  12. indico_plugin_stsa-0.1.0/indico_stsa/client/js/config.js +44 -0
  13. indico_plugin_stsa-0.1.0/indico_stsa/client/js/draft.js +121 -0
  14. indico_plugin_stsa-0.1.0/indico_stsa/client/js/index.jsx +26 -0
  15. indico_plugin_stsa-0.1.0/indico_stsa/client/js/stsa.module.scss +39 -0
  16. indico_plugin_stsa-0.1.0/indico_stsa/client/js/useDraft.js +101 -0
  17. indico_plugin_stsa-0.1.0/indico_stsa/client/js/wallet.js +138 -0
  18. indico_plugin_stsa-0.1.0/indico_stsa/client/styles/main.scss +46 -0
  19. indico_plugin_stsa-0.1.0/indico_stsa/constants.py +37 -0
  20. indico_plugin_stsa-0.1.0/indico_stsa/controllers/__init__.py +0 -0
  21. indico_plugin_stsa-0.1.0/indico_stsa/controllers/management.py +156 -0
  22. indico_plugin_stsa-0.1.0/indico_stsa/discount.py +89 -0
  23. indico_plugin_stsa-0.1.0/indico_stsa/emails.py +44 -0
  24. indico_plugin_stsa-0.1.0/indico_stsa/fields.py +76 -0
  25. indico_plugin_stsa-0.1.0/indico_stsa/forms.py +86 -0
  26. indico_plugin_stsa-0.1.0/indico_stsa/handlers.py +84 -0
  27. indico_plugin_stsa-0.1.0/indico_stsa/migrations/20260901_0000_b7c1d2e3f4a5_create_stsa_tables.py +41 -0
  28. indico_plugin_stsa-0.1.0/indico_stsa/models/__init__.py +5 -0
  29. indico_plugin_stsa-0.1.0/indico_stsa/models/settings.py +85 -0
  30. indico_plugin_stsa-0.1.0/indico_stsa/plugin.py +251 -0
  31. indico_plugin_stsa-0.1.0/indico_stsa/pricing.py +146 -0
  32. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/css/main.ed7cf087.css +4 -0
  33. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/css/main.ed7cf087.css.map +1 -0
  34. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/main.ff21f8cf.bundle.js +2 -0
  35. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/main.ff21f8cf.bundle.js.map +1 -0
  36. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/monaco.css.worker.js +8 -0
  37. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/monaco.css.worker.js.map +1 -0
  38. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/monaco.editor.worker.js +2 -0
  39. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/monaco.editor.worker.js.map +1 -0
  40. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/monaco.html.worker.js +8 -0
  41. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/js/monaco.html.worker.js.map +1 -0
  42. indico_plugin_stsa-0.1.0/indico_stsa/static/dist/manifest.json +9 -0
  43. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/README.md +40 -0
  44. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/apple/README.md +41 -0
  45. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/br.png +0 -0
  46. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/br.svg +16 -0
  47. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/cz.png +0 -0
  48. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/cz.svg +16 -0
  49. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/de.png +0 -0
  50. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/de.svg +16 -0
  51. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enAU.png +0 -0
  52. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enAU.svg +16 -0
  53. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enCA.png +0 -0
  54. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enCA.svg +16 -0
  55. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enGB.png +0 -0
  56. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enGB.svg +16 -0
  57. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enIN.png +0 -0
  58. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enIN.svg +16 -0
  59. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enSG.png +0 -0
  60. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enSG.svg +16 -0
  61. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enUS.png +0 -0
  62. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/enUS.svg +16 -0
  63. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/esES.png +0 -0
  64. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/esES.svg +16 -0
  65. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/frFR.png +0 -0
  66. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/frFR.svg +16 -0
  67. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/hu.png +0 -0
  68. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/hu.svg +16 -0
  69. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/id.png +0 -0
  70. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/id.svg +16 -0
  71. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/it.png +0 -0
  72. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/it.svg +16 -0
  73. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/jp.png +0 -0
  74. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/jp.svg +16 -0
  75. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/mn.png +0 -0
  76. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/mn.svg +10 -0
  77. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/nl.png +0 -0
  78. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/nl.svg +16 -0
  79. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/pl.png +0 -0
  80. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/pl.svg +16 -0
  81. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/pt.png +0 -0
  82. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/pt.svg +16 -0
  83. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/ru.png +0 -0
  84. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/ru.svg +16 -0
  85. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/se.png +0 -0
  86. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/se.svg +16 -0
  87. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/th.png +0 -0
  88. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/th.svg +16 -0
  89. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/tr.png +0 -0
  90. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/tr.svg +16 -0
  91. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/uk.png +0 -0
  92. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/uk.svg +16 -0
  93. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/vi.png +0 -0
  94. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/vi.svg +16 -0
  95. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/zhHK.png +0 -0
  96. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/zhHK.svg +16 -0
  97. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/zhTW.png +0 -0
  98. indico_plugin_stsa-0.1.0/indico_stsa/static/wallet/google/zhTW.svg +16 -0
  99. indico_plugin_stsa-0.1.0/indico_stsa/templates/_regform_settings.html +23 -0
  100. indico_plugin_stsa-0.1.0/indico_stsa/templates/overview.html +58 -0
  101. indico_plugin_stsa-0.1.0/indico_stsa/templates/settings.html +74 -0
  102. indico_plugin_stsa-0.1.0/indico_stsa/ticket_email.py +165 -0
  103. indico_plugin_stsa-0.1.0/indico_stsa/util.py +196 -0
  104. indico_plugin_stsa-0.1.0/indico_stsa/wallet.py +135 -0
  105. indico_plugin_stsa-0.1.0/pyproject.toml +74 -0
  106. indico_plugin_stsa-0.1.0/ruff.toml +78 -0
  107. indico_plugin_stsa-0.1.0/scripts/install-apple-badges.py +80 -0
  108. indico_plugin_stsa-0.1.0/tests/test_discount.py +85 -0
  109. indico_plugin_stsa-0.1.0/tests/test_emails.py +49 -0
  110. indico_plugin_stsa-0.1.0/tests/test_wallet.py +128 -0
  111. indico_plugin_stsa-0.1.0/webpack-bundles.json +5 -0
@@ -0,0 +1,85 @@
1
+ """Check that a built wheel carries everything the plugin needs -- and nothing it may not.
2
+
3
+ A wheel missing its assets, templates or migrations installs cleanly and then
4
+ fails on the first request that touches them, which is a much worse way to find
5
+ out. The asset check is the one that matters most: `static/dist/manifest.json`
6
+ is what `IndicoPlugin.inject_bundle` looks `main.js` and `main.css` up in, and
7
+ without it Indico raises `Assets for plugin stsa have not been built` on every
8
+ registration page in the instance.
9
+
10
+ The last check runs the other way round. Apple's "Add to Apple Wallet" artwork
11
+ is licensed to whoever downloaded it, under a non-transferable licence covering
12
+ only their own passes, so it must never leave a maintainer's machine inside a
13
+ package. It is gitignored and excluded in `pyproject.toml`; this is the gate
14
+ that stops a release if both of those are ever undone.
15
+ """
16
+
17
+ import json
18
+ import sys
19
+ import zipfile
20
+ from pathlib import Path
21
+
22
+
23
+ PKG = 'indico_stsa'
24
+
25
+ REQUIRED_FILES = [
26
+ f'{PKG}/templates/settings.html',
27
+ f'{PKG}/templates/overview.html',
28
+ f'{PKG}/templates/_regform_settings.html',
29
+ f'{PKG}/static/dist/manifest.json',
30
+ # Google's button artwork is redistributable and the plugin is useless
31
+ # without at least the fallback locale.
32
+ f'{PKG}/static/wallet/google/enGB.svg',
33
+ f'{PKG}/static/wallet/google/enGB.png',
34
+ # The instructions for the artwork that is *not* here.
35
+ f'{PKG}/static/wallet/apple/README.md',
36
+ ]
37
+
38
+ # Filenames here carry a date or a content hash, so match on directory and
39
+ # extension rather than on a name that changes every build.
40
+ REQUIRED_PATTERNS = [
41
+ (f'{PKG}/migrations/', '.py'),
42
+ (f'{PKG}/static/dist/', '.js'),
43
+ (f'{PKG}/static/dist/', '.css'),
44
+ ]
45
+
46
+ # The exact keys `plugin.py` passes to `inject_bundle`.
47
+ REQUIRED_BUNDLES = ['main.js', 'main.css']
48
+
49
+ # Nothing matching these may be in the wheel. See the module docstring.
50
+ FORBIDDEN_PATTERNS = [
51
+ (f'{PKG}/static/wallet/apple/', '.svg'),
52
+ (f'{PKG}/static/wallet/apple/', '.png'),
53
+ ]
54
+
55
+
56
+ def main(dist_dir):
57
+ wheel = next(Path(dist_dir).glob('*.whl'), None)
58
+ if wheel is None:
59
+ sys.exit(f'no wheel in {dist_dir}')
60
+
61
+ with zipfile.ZipFile(wheel) as zf:
62
+ names = set(zf.namelist())
63
+ problems = [f'missing {name}' for name in REQUIRED_FILES if name not in names]
64
+ for prefix, suffix in REQUIRED_PATTERNS:
65
+ if not any(n.startswith(prefix) and n.endswith(suffix) for n in names):
66
+ problems.append(f'missing {prefix}*{suffix}')
67
+
68
+ for prefix, suffix in FORBIDDEN_PATTERNS:
69
+ found = sorted(n for n in names if n.startswith(prefix) and n.endswith(suffix))
70
+ if found:
71
+ problems.append("Apple's badge artwork is not redistributable and must not be packaged: "
72
+ + ', '.join(found))
73
+
74
+ if f'{PKG}/static/dist/manifest.json' in names:
75
+ manifest = json.loads(zf.read(f'{PKG}/static/dist/manifest.json'))
76
+ problems += [f'manifest.json has no {bundle!r} entry'
77
+ for bundle in REQUIRED_BUNDLES if bundle not in manifest]
78
+
79
+ if problems:
80
+ sys.exit('{}:\n {}'.format(wheel.name, '\n '.join(problems)))
81
+ print(f'{wheel.name} looks good')
82
+
83
+
84
+ if __name__ == '__main__':
85
+ main(sys.argv[1] if len(sys.argv) > 1 else 'dist')
@@ -0,0 +1,30 @@
1
+ # Playwright MCP drops snapshots and downloads wherever it is run from.
2
+ .playwright-mcp/
3
+
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.egg-info/
7
+ .eggs/
8
+ build/
9
+ dist/
10
+ .venv/
11
+ venv/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .coverage
15
+ coverage.xml
16
+ htmlcov/
17
+ *.mo
18
+
19
+ # Plugin asset build artifacts
20
+ node_modules/
21
+ indico_stsa/static/dist/
22
+ webpack-build-config.json
23
+ url_map.json
24
+
25
+ # Apple's badge artwork is licensed to the operator, not to this package: the
26
+ # Wallet Marketing Agreement is non-transferable and covers only the licensee's
27
+ # own passes. Each deployment installs its own copy with
28
+ # `scripts/install-apple-badges.py`; it must never be committed or published.
29
+ indico_stsa/static/wallet/apple/*.svg
30
+ indico_stsa/static/wallet/apple/*.png
@@ -0,0 +1,32 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Singapore Taiwanese Student Association
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The "Add to Google Wallet" button artwork under
26
+ `indico_stsa/static/wallet/google/` is Google's, not covered by the licence
27
+ above, and is redistributed here under Google's Wallet brand guidelines. It may
28
+ be used only to link to a Google Wallet pass.
29
+
30
+ The "Add to Apple Wallet" badge is deliberately NOT included in this
31
+ repository: Apple licenses it per operator under a non-transferable agreement.
32
+ See `indico_stsa/static/wallet/apple/README.md`.
@@ -0,0 +1,319 @@
1
+ Metadata-Version: 2.4
2
+ Name: indico-plugin-stsa
3
+ Version: 0.1.0
4
+ Summary: STSA customizations for Indico: e-mail subject prefixes, a member discount, and the standard wallet buttons
5
+ Project-URL: Homepage, https://github.com/stsa-tw/IndicoSTSA
6
+ Project-URL: Source, https://github.com/stsa-tw/IndicoSTSA
7
+ Project-URL: Issues, https://github.com/stsa-tw/IndicoSTSA/issues
8
+ Author: Singapore Taiwanese Student Association
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Environment :: Plugins
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: <3.13,>=3.12.2
16
+ Requires-Dist: indico<3.4,>=3.3
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
19
+ Requires-Dist: pytest>=8; extra == 'dev'
20
+ Requires-Dist: ruff>=0.8; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Indico STSA plugin
24
+
25
+ Customizations for the Singapore Taiwanese Student Association.
26
+
27
+ Three things, all optional and all off until somebody switches them on:
28
+
29
+ 1. **E-mail subject prefixes.** Every mail Indico sends goes out with STSA's own
30
+ prefix instead of `[Indico]`.
31
+ 2. **A member discount, per registration form.** Money off for anyone who
32
+ registers while signed in with their STSA membership — with the answers they
33
+ have already typed kept intact across the trip to the login page.
34
+ 3. **Members-only group registration.** Where the
35
+ [group registration plugin](https://github.com/RobotHanzo/IndicoGroupRegistration)
36
+ is also installed, creating or joining a group can be restricted to
37
+ signed-in members.
38
+ 4. **The standard wallet buttons.** Indico's "Add to Wallet" dropdown is
39
+ replaced with Apple's and Google's own badges, on the registration page and
40
+ in the e-mail the ticket arrives with.
41
+
42
+ A **member** is anyone signed in. An STSA membership *is* an account on the
43
+ site: there is no membership table and no separate sign-up, so "becoming a
44
+ member" is exactly "signing in". Every participant-facing string says "STSA
45
+ membership" rather than naming Indico.
46
+
47
+ ## Installing
48
+
49
+ ```bash
50
+ pip install indico-plugin-stsa
51
+ ```
52
+
53
+ Then add it to `PLUGINS` in `indico.conf`:
54
+
55
+ ```python
56
+ PLUGINS = {'stsa'}
57
+ ```
58
+
59
+ and create its tables:
60
+
61
+ ```bash
62
+ indico db --plugin stsa upgrade
63
+ ```
64
+
65
+ Restart Indico (and the Celery worker, if you run one).
66
+
67
+ Released wheels carry the compiled webpack bundle. Installing straight from a
68
+ git checkout does not — see [Building the assets](#building-the-assets).
69
+
70
+ ## 1. E-mail subject prefixes
71
+
72
+ Configured once, for the whole site, at **Administration → Plugins → STSA**:
73
+
74
+ | Setting | Default | |
75
+ | --- | --- | --- |
76
+ | Rewrite e-mail subject prefixes | on | The master switch. |
77
+ | Subject prefix | `[STSA 活動]` | What `[Indico]` becomes. Empty strips it. |
78
+ | Use the Apple and Google wallet badges | on | See [the wallet buttons](#4-the-wallet-buttons). |
79
+
80
+ Every outgoing mail is covered: registration confirmations, password resets,
81
+ event reminders, abstract notifications, and anything a plugin sends. Mails
82
+ that deliberately carry *no* prefix — the room booking ones set the prefix block
83
+ to empty — are left exactly as they are, because there is nothing there to
84
+ replace. The error-report mail's `[Indico@yourserver]` is replaced whole; the
85
+ server name it carries is in the body anyway.
86
+
87
+ ### How
88
+
89
+ Indico builds every mail through `indico.core.notifications.make_email`, which
90
+ is decorated with `@make_interceptable` precisely so that a plugin can step in.
91
+ The plugin intercepts it, lets Indico build the mail as usual, and rewrites the
92
+ subject afterwards.
93
+
94
+ The obvious alternative — overriding `emails/base.txt` and `emails/base.html`
95
+ through `get_template_customization_paths` — was rejected. It only reaches the
96
+ mails that go through those templates and do not override the `subject_prefix`
97
+ block themselves, it misses every mail built from a plain `subject=` string, and
98
+ the value would be fixed at deploy time rather than configurable.
99
+
100
+ If the setting cannot be read, or the rewrite raises for any reason, the
101
+ original subject is used and the mail still goes out. A misconfigured prefix
102
+ must never cost somebody their registration confirmation.
103
+
104
+ ## 2. The member discount
105
+
106
+ Per registration form, at **Event → Registration → STSA**, or from the
107
+ **STSA** box on the registration form's own settings page.
108
+
109
+ | Setting | |
110
+ | --- | --- |
111
+ | Enable the member discount | Off by default. |
112
+ | Discount | A percentage, or a fixed amount in the form's currency. |
113
+ | Discount applies to | The registration fee only, or the whole price including paid options. |
114
+ | Sign-in notice | Optional. Replaces the built-in wording of the notice. |
115
+
116
+ ### What the participant sees
117
+
118
+ Somebody who is **not signed in** gets a notice above the form saying what the
119
+ discount is worth and a prominent **Sign in with your STSA membership** button.
120
+ Following it takes them through the login page and straight back to the form,
121
+ with everything they had already typed still in place.
122
+
123
+ Somebody who **is signed in** sees nothing extra. The discount appears as a
124
+ named *Member discount* line on their invoice, their registration summary and
125
+ their confirmation e-mail, with the rate next to it.
126
+
127
+ ### Keeping the answers
128
+
129
+ The answers go into `sessionStorage` when the page is navigated away from
130
+ (`pagehide`, which also covers signing in through the header menu rather than
131
+ the plugin's own link) and are read back — once, then deleted — when the form
132
+ next loads. A draft older than six hours is ignored: that is an abandoned tab,
133
+ not a login round trip.
134
+
135
+ Two things deliberately do not survive: **file uploads**, because the browser
136
+ will not let a `File` be put back, and the **captcha answer**, because the
137
+ challenge it belongs to is gone. Both fields are simply left untouched.
138
+
139
+ ### Who counts as a member
140
+
141
+ A registration earns the discount when it is *the account holder's own*. Both
142
+ halves of that matter:
143
+
144
+ - `registration.user` alone is not enough. Core sets it from the **e-mail
145
+ address**, with `get_user_by_email`, so on its own it would hand the discount
146
+ to anybody who types a member's address into an anonymous registration.
147
+ - `session.user` alone is not enough either. It would hand the discount to a
148
+ signed-in organizer registering somebody else.
149
+
150
+ So the test is `registration.user is not None and session.user == registration.user`.
151
+ Registrations created from the management area are trusted instead: an organizer
152
+ adding a participant has made that decision deliberately.
153
+
154
+ Once earned, the discount **sticks**. Modifying a registration only ever adds
155
+ the discount, never removes it — a participant may well be editing from the link
156
+ in their confirmation e-mail with nobody signed in, and losing a discount over a
157
+ changed phone number would be indefensible. Switching the discount off on the
158
+ form does remove it, on the next change to each registration.
159
+
160
+ ### Applying it to registrations that already exist
161
+
162
+ Saving the settings only affects registrations made from then on. The
163
+ **Apply to existing registrations** button on the settings page reprices
164
+ everything on the form, and is deliberately a button rather than a side effect
165
+ of saving: it treats *every* registration linked to a membership account as a
166
+ member's, including ones made before the discount existed. Somebody whose price
167
+ goes up after they have paid will owe the difference, and Indico has no
168
+ partial-payment concept to collect it with.
169
+
170
+ ## 3. Members-only group registration
171
+
172
+ Needs the group registration plugin. The switch is on the same STSA settings
173
+ page, and is hidden when that plugin is not installed — a switch that currently
174
+ gates nothing should not be offered.
175
+
176
+ With it on, a participant who is not signed in finds the group plan picker
177
+ inert, with a note underneath saying groups are for STSA members and offering
178
+ the same sign-in button. A code arriving from a shared join link
179
+ (`?group_code=…`) is cleared rather than acted on.
180
+
181
+ The server refuses the same choice again when the registration is submitted, and
182
+ that is the half that enforces the rule. The check reads the *submitted* answer
183
+ rather than the stored one, which is what makes it independent of whether the
184
+ group plugin's own signal handler has already run: reading the stored answer
185
+ would either block every later edit of a registration that is already in a
186
+ group, or let the very thing this is meant to stop straight through, depending
187
+ on which plugin's handler happened to run first.
188
+
189
+ Organizers are never gated. Adding a participant to a group from the management
190
+ area is a deliberate act by somebody who is already signed in.
191
+
192
+ ## 4. The wallet buttons
193
+
194
+ Where an organizer has configured the passes, Indico offers them as an
195
+ **Add to Wallet** dropdown — or, if only one wallet is set up, as an ordinary
196
+ Indico button. Neither is what people look for, and both vendors' guidelines ask
197
+ for their own badge instead. So the plugin swaps them for the real thing, in two
198
+ places: on the registration page, and in the e-mail the ticket arrives with.
199
+
200
+ ### The artwork
201
+
202
+ Both badges are the vendors' **official** artwork; neither is drawn by hand,
203
+ because both vendors forbid recreating theirs. But only one of them can ship
204
+ here:
205
+
206
+ | | Ships with the plugin? | Source | Languages |
207
+ | --- | --- | --- | --- |
208
+ | Google | **yes** | the asset packs published with its [brand guidelines](https://developers.google.com/wallet/generic/resources/brand-guidelines) | 27 |
209
+ | Apple | **no — you install it** | the pack behind **Download badge files** on its [badge guidelines](https://developer.apple.com/wallet/add-to-apple-wallet-guidelines/) | 24 |
210
+
211
+ Apple's Wallet Marketing Artwork License Agreement grants a *non-transferable*
212
+ licence to use the badge **only in connection with your own passes**, so this
213
+ package has no right to hand it on to whoever installs it next. Each deployment
214
+ downloads its own copy — a formality for anyone already issuing Apple passes,
215
+ since that needs an Apple Developer account regardless — and installs it with:
216
+
217
+ ```bash
218
+ python scripts/install-apple-badges.py ~/Downloads/Add-to-Apple-Wallet.zip
219
+ ```
220
+
221
+ Until then the Apple link keeps the button Indico shipped. Nothing is
222
+ substituted for it. `indico_stsa/static/wallet/apple/README.md` has the details,
223
+ and a test plus a release gate make sure the artwork never ends up in a package
224
+ by accident.
225
+
226
+ Apple ships SVG and EPS but no PNG, and e-mail needs one — Gmail and Outlook do
227
+ not render SVG at all — so the installer rasterizes Apple's own SVGs at 3× the
228
+ rendered height, leaving proportions and colours untouched.
229
+
230
+ The badge matching the reader's Indico language is used, falling back to
231
+ English. Both are rendered at 48px, Google's stated minimum, with clear space
232
+ around them and no hover or press effect — both vendors forbid altering their
233
+ artwork, and Apple names dimming and animation specifically.
234
+
235
+ ### On the page
236
+
237
+ A small script rewrites the finished page: it finds the links pointing at
238
+ Indico's two wallet endpoints, puts a badge for each in a row below the action
239
+ box, and removes the dropdown once nothing is left in it. The badge keeps the
240
+ href core gave the link, so it grants exactly the access core intended.
241
+
242
+ The alternative was forking `registration_summary.html`, a 300-line core
243
+ template that changes between Indico releases and would then have to be reviewed
244
+ on every upgrade for the sake of two buttons. Working on the DOM also fails
245
+ safely: if core moves these endpoints, the links are simply not found and the
246
+ participant keeps the buttons Indico shipped.
247
+
248
+ The badges go in a row of their own rather than into the toolbar. That toolbar
249
+ is a single non-wrapping flex row sized for Indico's own 30px buttons, and two
250
+ 48px badges pushed into it slide out past the edge of the box.
251
+
252
+ ### In the e-mail
253
+
254
+ The same badges are added to the mail the ticket is attached to — which is where
255
+ somebody who has just been sent a ticket is actually looking. They appear on
256
+ exactly the mails that carry the ticket and no others: the condition mirrors the
257
+ one in Indico's `_notify_registration`, so an organizer who has switched ticket
258
+ e-mails off does not start getting pass links instead.
259
+
260
+ The images are **embedded** rather than linked, because most mail clients block
261
+ remote images until the reader asks for them and a blocked button is a button
262
+ nobody presses. Indico already sends inline images this way for registration
263
+ pictures. The links carry the registration's token, so they work without
264
+ signing in.
265
+
266
+ The mail is already rendered by the time the plugin sees it, so the block is
267
+ spliced in just above the card's grey footer. If a future Indico changes that
268
+ footer the badges land after the card instead — untidy, but still a working
269
+ button.
270
+
271
+ ## Working with the group registration plugin
272
+
273
+ Both plugins put a negative line item on the registration, and both compute it
274
+ from "everything billable except my own line". Left alone, each would be
275
+ calculated from the other and every recomputation would move both.
276
+
277
+ So the member discount is calculated from the registration fee and the paid
278
+ options **only** — both discount lines are excluded from its basis. The group
279
+ discount is then free to stack on top of it, and whenever the member discount
280
+ changes, the plugin asks the group plugin to recompute the group's line so it is
281
+ never one registration out of date. The dependency runs one way, so it cannot
282
+ loop.
283
+
284
+ Neither plugin can push a registration below zero: each clamps its own line to
285
+ the amount it applies to, and Indico clamps the total at zero regardless.
286
+
287
+ The plugin works perfectly well without the group registration plugin; the group
288
+ switch simply disappears.
289
+
290
+ ## Building the assets
291
+
292
+ Released wheels already carry the compiled bundle. From a git checkout you need
293
+ an Indico *source* tree at the version the plugin runs against:
294
+
295
+ ```bash
296
+ git clone --branch v3.3.13 https://github.com/indico/indico ~/dev/indico
297
+ (cd ~/dev/indico && npm ci)
298
+
299
+ /opt/indico/.venv/bin/python build-assets.py --indico-source ~/dev/indico
300
+ ```
301
+
302
+ Run it with the Python that Indico and this plugin are installed into: the build
303
+ resolves the plugin's own URL rules by importing it. `--dev` and `--watch` are
304
+ passed through.
305
+
306
+ ## Tests
307
+
308
+ ```bash
309
+ pip install -e '.[dev]'
310
+ pytest
311
+ ```
312
+
313
+ The unit tests cover the parts that hold the arithmetic and the string handling
314
+ and need neither Indico nor a database. Everything that talks to the database is
315
+ exercised against a real Indico instance.
316
+
317
+ ## Licence
318
+
319
+ MIT.