libreqda 1.2.0__py3-none-any.whl

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 (255) hide show
  1. CHANGELOG.md +481 -0
  2. libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
  3. libreqda-1.2.0.dist-info/METADATA +131 -0
  4. libreqda-1.2.0.dist-info/RECORD +255 -0
  5. libreqda-1.2.0.dist-info/WHEEL +4 -0
  6. libreqda-1.2.0.dist-info/entry_points.txt +4 -0
  7. scripts/babelrc +8 -0
  8. scripts/dist/LICENSE.txt +33 -0
  9. scripts/dist/RELEASE.txt +23 -0
  10. scripts/dist/macos/libreqda.icns +0 -0
  11. scripts/dist/macos/macos.spec +43 -0
  12. scripts/dist/macos/plist.patch +10 -0
  13. scripts/dist/macos/taguette.icns +0 -0
  14. scripts/dist/macos/taguette_console_wrapper +4 -0
  15. scripts/dist/macos.sh +32 -0
  16. scripts/dist/pyinstaller_entrypoint.py +19 -0
  17. scripts/dist/windows/windows-installer.iss +51 -0
  18. scripts/dist/windows/windows.spec +39 -0
  19. scripts/edugain/worker_update_edugain_idps.py +89 -0
  20. scripts/edugain/worker_update_edugain_idps.sh +9 -0
  21. scripts/make_password_reset_link.py +56 -0
  22. scripts/make_theme.py +99 -0
  23. scripts/new_db_revision.sh +8 -0
  24. scripts/register_user.py +76 -0
  25. scripts/update_pot.sh +33 -0
  26. scripts/update_translations.sh +17 -0
  27. scripts/validate.py +140 -0
  28. scripts/wipe_and_disable_user.py +90 -0
  29. taguette/__init__.py +13 -0
  30. taguette/access_control.py +145 -0
  31. taguette/convert.py +623 -0
  32. taguette/database/__init__.py +177 -0
  33. taguette/database/base.py +9 -0
  34. taguette/database/copy.py +433 -0
  35. taguette/database/models.py +920 -0
  36. taguette/demodata.py +688 -0
  37. taguette/export.py +500 -0
  38. taguette/extract.py +287 -0
  39. taguette/import_codebook.py +110 -0
  40. taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
  41. taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
  42. taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
  43. taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
  44. taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
  45. taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
  46. taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
  47. taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
  48. taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
  49. taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
  50. taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
  51. taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
  52. taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
  53. taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
  54. taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
  55. taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
  56. taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
  57. taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
  58. taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
  59. taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
  60. taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
  61. taguette/loki_handler.py +107 -0
  62. taguette/main.py +649 -0
  63. taguette/migrations/env.py +72 -0
  64. taguette/migrations/script.py.mako +24 -0
  65. taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
  66. taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
  67. taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
  68. taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
  69. taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
  70. taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
  71. taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
  72. taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
  73. taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
  74. taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
  75. taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
  76. taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
  77. taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
  78. taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
  79. taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
  80. taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
  81. taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
  82. taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
  83. taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
  84. taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
  85. taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
  86. taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
  87. taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
  88. taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
  89. taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
  90. taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
  91. taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
  92. taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
  93. taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
  94. taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
  95. taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
  96. taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
  97. taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
  98. taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
  99. taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
  100. taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
  101. taguette/static/README.md +34 -0
  102. taguette/static/alt_templates/.gitkeep +0 -0
  103. taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
  104. taguette/static/css/custom.css +1696 -0
  105. taguette/static/css/fa-all.min.css +5 -0
  106. taguette/static/css/themed.css +3 -0
  107. taguette/static/css/themed.css.map +25 -0
  108. taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
  109. taguette/static/css/vendor/bootstrap.min.css +6 -0
  110. taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
  111. taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
  112. taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
  113. taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
  114. taguette/static/css/vendor/hierarchy-select.min.css +1 -0
  115. taguette/static/css/vendor/introjs.min.css +2 -0
  116. taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
  117. taguette/static/favicon.ico +0 -0
  118. taguette/static/js/codebook-viz.js +428 -0
  119. taguette/static/js/collection/context-menu.js +180 -0
  120. taguette/static/js/collection/hierarchy-select.js +71 -0
  121. taguette/static/js/collection/manager.js +217 -0
  122. taguette/static/js/document/context-menu.js +92 -0
  123. taguette/static/js/document/custom-select.js +82 -0
  124. taguette/static/js/document/manager.js +943 -0
  125. taguette/static/js/document/treeview.js +593 -0
  126. taguette/static/js/highlight/README.md +18 -0
  127. taguette/static/js/highlight/crud.js +119 -0
  128. taguette/static/js/highlight/extracts.js +497 -0
  129. taguette/static/js/highlight/loader.js +109 -0
  130. taguette/static/js/highlight/renderer.js +405 -0
  131. taguette/static/js/login.js +15 -0
  132. taguette/static/js/permissions.js +153 -0
  133. taguette/static/js/polling-event.js +145 -0
  134. taguette/static/js/project/intro.js +524 -0
  135. taguette/static/js/project/manager.js +0 -0
  136. taguette/static/js/project/search.js +124 -0
  137. taguette/static/js/tag/context-menu.js +133 -0
  138. taguette/static/js/tag/explore-codes.js +400 -0
  139. taguette/static/js/tag/hierarchy-select.js +69 -0
  140. taguette/static/js/tag/manager.js +545 -0
  141. taguette/static/js/tag/treeview.js +613 -0
  142. taguette/static/js/taguette.js +1790 -0
  143. taguette/static/js/utilities.js +714 -0
  144. taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
  145. taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
  146. taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
  147. taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
  148. taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
  149. taguette/static/js/vendor/hierarchy-select.min.js +1 -0
  150. taguette/static/js/vendor/introjs.min.js +17 -0
  151. taguette/static/js/vendor/jquery-3.7.1.js +2 -0
  152. taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
  153. taguette/static/libreqda.ico +0 -0
  154. taguette/static/logo-h.png +0 -0
  155. taguette/static/logo.png +0 -0
  156. taguette/static/logo_blanc.png +0 -0
  157. taguette/static/missing.png +0 -0
  158. taguette/static/robots.txt +2 -0
  159. taguette/static/scss/components/_badge.scss +42 -0
  160. taguette/static/scss/components/_button.scss +169 -0
  161. taguette/static/scss/components/_checkbox.scss +40 -0
  162. taguette/static/scss/components/_context_menu.scss +28 -0
  163. taguette/static/scss/components/_custom_select.scss +150 -0
  164. taguette/static/scss/components/_dropdown.scss +43 -0
  165. taguette/static/scss/components/_form.scss +54 -0
  166. taguette/static/scss/components/_modal.scss +57 -0
  167. taguette/static/scss/components/_project_card.scss +163 -0
  168. taguette/static/scss/components/_tag.scss +308 -0
  169. taguette/static/scss/components/_treeview.scss +76 -0
  170. taguette/static/scss/custom.scss +809 -0
  171. taguette/static/scss/layout/_credits.scss +62 -0
  172. taguette/static/scss/layout/_footer.scss +6 -0
  173. taguette/static/scss/layout/_navigation.scss +165 -0
  174. taguette/static/scss/layout/_sidebar.scss +37 -0
  175. taguette/static/scss/themes/default/_variables.scss +103 -0
  176. taguette/static/scss/themes/default/theme.css +3 -0
  177. taguette/static/scss/themes/default/theme.css.map +26 -0
  178. taguette/static/scss/themes/default/theme.scss +4 -0
  179. taguette/static/webfonts/BadScript-OFL.txt +94 -0
  180. taguette/static/webfonts/BadScript.ttf +0 -0
  181. taguette/static/webfonts/Montserrat-Light.ttf +0 -0
  182. taguette/static/webfonts/Montserrat-OFL.txt +93 -0
  183. taguette/static/webfonts/fa-brands-400.eot +0 -0
  184. taguette/static/webfonts/fa-brands-400.svg +1184 -0
  185. taguette/static/webfonts/fa-brands-400.ttf +0 -0
  186. taguette/static/webfonts/fa-brands-400.woff +0 -0
  187. taguette/static/webfonts/fa-brands-400.woff2 +0 -0
  188. taguette/static/webfonts/fa-regular-400.eot +0 -0
  189. taguette/static/webfonts/fa-regular-400.svg +467 -0
  190. taguette/static/webfonts/fa-regular-400.ttf +0 -0
  191. taguette/static/webfonts/fa-regular-400.woff +0 -0
  192. taguette/static/webfonts/fa-regular-400.woff2 +0 -0
  193. taguette/static/webfonts/fa-solid-900.eot +0 -0
  194. taguette/static/webfonts/fa-solid-900.svg +2618 -0
  195. taguette/static/webfonts/fa-solid-900.ttf +0 -0
  196. taguette/static/webfonts/fa-solid-900.woff +0 -0
  197. taguette/static/webfonts/fa-solid-900.woff2 +0 -0
  198. taguette/templates/alt_templates/.gitkeep +0 -0
  199. taguette/templates/archived_projects.html +50 -0
  200. taguette/templates/auth/account.html +74 -0
  201. taguette/templates/auth/cilogon.login.html +89 -0
  202. taguette/templates/auth/email_reset_password.html +5 -0
  203. taguette/templates/auth/external_account.html +60 -0
  204. taguette/templates/auth/login.html +144 -0
  205. taguette/templates/auth/reset_password.html +27 -0
  206. taguette/templates/auth/token_needed.html +7 -0
  207. taguette/templates/base.html +209 -0
  208. taguette/templates/bug_report.html +158 -0
  209. taguette/templates/cookies_prompt.html +15 -0
  210. taguette/templates/downloads.html +127 -0
  211. taguette/templates/email_bug_report.txt +9 -0
  212. taguette/templates/email_reset_password.txt +5 -0
  213. taguette/templates/error.html +8 -0
  214. taguette/templates/export_codebook.html +20 -0
  215. taguette/templates/export_document.html +20 -0
  216. taguette/templates/export_highlights.html +47 -0
  217. taguette/templates/index.html +347 -0
  218. taguette/templates/maintenance.html +8 -0
  219. taguette/templates/messages.js +74 -0
  220. taguette/templates/modal/change_document.html +68 -0
  221. taguette/templates/modal/create_collection.html +100 -0
  222. taguette/templates/modal/create_document.html +84 -0
  223. taguette/templates/modal/create_highlight.html +38 -0
  224. taguette/templates/modal/create_tag.html +73 -0
  225. taguette/templates/modal/explore_codes.html +58 -0
  226. taguette/templates/modal/manage_collaborators.html +44 -0
  227. taguette/templates/modal/merge_tag.html +49 -0
  228. taguette/templates/new_password.html +32 -0
  229. taguette/templates/partials/alert.html +16 -0
  230. taguette/templates/partials/codebook_viz_modal.html +34 -0
  231. taguette/templates/partials/confirm.html +17 -0
  232. taguette/templates/partials/document_toolbar.html +37 -0
  233. taguette/templates/partials/hlinfo.html +13 -0
  234. taguette/templates/partials/project_card.html +44 -0
  235. taguette/templates/partials/project_card_archived.html +41 -0
  236. taguette/templates/partials/project_sidebar.html +99 -0
  237. taguette/templates/partials/spinner.html +11 -0
  238. taguette/templates/permissions.html +54 -0
  239. taguette/templates/project.html +288 -0
  240. taguette/templates/project_delete.html +17 -0
  241. taguette/templates/project_import.html +122 -0
  242. taguette/templates/project_import_codebook.html +34 -0
  243. taguette/templates/project_import_codebook_confirm.html +49 -0
  244. taguette/templates/project_new.html +31 -0
  245. taguette/templates/tos.html +8 -0
  246. taguette/templates/trans.js +7 -0
  247. taguette/templates/welcome.html +15 -0
  248. taguette/utils.py +177 -0
  249. taguette/validate.py +141 -0
  250. taguette/version_check.py +102 -0
  251. taguette/web/__init__.py +252 -0
  252. taguette/web/api.py +1835 -0
  253. taguette/web/base.py +864 -0
  254. taguette/web/export.py +338 -0
  255. taguette/web/views.py +1618 -0
@@ -0,0 +1,920 @@
1
+ import asyncio
2
+ import binascii
3
+ from datetime import datetime
4
+ import enum
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import logging
9
+ import opentelemetry.trace
10
+ import os
11
+ from sqlalchemy import (Column, func, ForeignKey,
12
+ Index, TypeDecorator, MetaData,
13
+ Table, UniqueConstraint,
14
+ select, distinct)
15
+ from sqlalchemy.ext.declarative import declarative_base
16
+ from sqlalchemy.orm import aliased, column_property, deferred, relationship
17
+ from sqlalchemy.sql import functions
18
+ from sqlalchemy.types import (Boolean, DateTime, Enum, Integer, String, Text,
19
+ BigInteger)
20
+ from sqlalchemy.ext.compiler import compiles
21
+ from sqlalchemy.dialects.postgresql import ARRAY
22
+ from .base import PROM_COMMAND
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+ tracer = opentelemetry.trace.get_tracer(__name__)
27
+
28
+
29
+ meta = MetaData(naming_convention={
30
+ "ix": "ix_%(column_0_label)s",
31
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
32
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
33
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
34
+ "pk": "pk_%(table_name)s",
35
+ })
36
+ Base = declarative_base(metadata=meta)
37
+
38
+
39
+ class JSON(TypeDecorator):
40
+ """Platform-independent JSON type.
41
+ """
42
+ impl = Text
43
+
44
+ def process_bind_param(self, value, dialect):
45
+ return json.dumps(value, sort_keys=True)
46
+
47
+ def process_result_value(self, value, dialect):
48
+ return json.loads(value)
49
+
50
+
51
+ SCRYPT_MAX_MEM = 64 << 20
52
+
53
+
54
+ class Roles(enum.Enum):
55
+ ADMIN = 1
56
+ PROJECT_ADMIN = 2
57
+ TEAM_MEMBER = 3
58
+ PROJECT_OWNER = 4
59
+
60
+ def can_create_project(self):
61
+ return self in (Roles.ADMIN, Roles.PROJECT_ADMIN, Roles.PROJECT_OWNER)
62
+
63
+ def can_change_role(self):
64
+ return self in (Roles.ADMIN, Roles.PROJECT_ADMIN)
65
+
66
+ def role_can_be_changed(self):
67
+ return self in (Roles.TEAM_MEMBER, Roles.PROJECT_OWNER)
68
+
69
+
70
+ class User(Base):
71
+ __tablename__ = 'users'
72
+
73
+ login = Column(String(255), primary_key=True)
74
+ displayname = Column(String(255), nullable=True, default="")
75
+ created = Column(DateTime, nullable=False,
76
+ default=lambda: datetime.utcnow())
77
+ hashed_password = Column(String(192), nullable=True)
78
+ disabled = Column(Boolean, nullable=False, default=False)
79
+ password_set_date = Column(DateTime, nullable=True)
80
+ language = Column(String(10), nullable=True)
81
+ hide_code_counts = Column(Boolean, nullable=True, default=False)
82
+ email = Column(String(256), nullable=True, index=True, unique=True)
83
+ email_sent = Column(DateTime, nullable=True)
84
+ external = Column(Boolean, nullable=True, default=False)
85
+ role = Column(Enum(Roles), nullable=False, default=Roles.ADMIN)
86
+ approved_by_user_login = Column(
87
+ String(255),
88
+ ForeignKey('users.login', ondelete='CASCADE'),
89
+ nullable=True,
90
+ index=True
91
+ )
92
+ approved_by_user = relationship('User')
93
+ project_memberships = relationship(
94
+ 'ProjectMember',
95
+ cascade='all,delete-orphan',
96
+ back_populates='user',
97
+ )
98
+
99
+ async def set_password(self, password, method=None):
100
+ if method is None:
101
+ try:
102
+ from hashlib import scrypt as _scrypt # noqa: F401
103
+ method = 'scrypt'
104
+ except ImportError:
105
+ method = 'pbkdf2'
106
+
107
+ if method == 'scrypt':
108
+ N = 1 << 15
109
+ R = 8
110
+ P = 1
111
+ with tracer.start_as_current_span(
112
+ 'taguette/set_password',
113
+ attributes={'method': method, 'factor': N},
114
+ ):
115
+ salt = os.urandom(16)
116
+ h = await asyncio.get_event_loop().run_in_executor(
117
+ None,
118
+ lambda: hashlib.scrypt(
119
+ password.encode('utf-8'),
120
+ salt=salt, n=N, r=R, p=P,
121
+ maxmem=SCRYPT_MAX_MEM,
122
+ ),
123
+ )
124
+ self.hashed_password = 'scrypt:%s$%d$%d$%d$%s' % (
125
+ binascii.hexlify(salt).decode('ascii'),
126
+ N, R, P,
127
+ binascii.hexlify(h).decode('ascii'),
128
+ )
129
+ elif method == 'pbkdf2':
130
+ ITERATIONS = 500000
131
+ with tracer.start_as_current_span(
132
+ 'taguette/set_password',
133
+ attributes={'method': method, 'iterations': ITERATIONS},
134
+ ):
135
+ salt = os.urandom(16)
136
+ h = await asyncio.get_event_loop().run_in_executor(
137
+ None,
138
+ lambda: hashlib.pbkdf2_hmac(
139
+ 'sha256', password.encode('utf-8'),
140
+ salt, ITERATIONS,
141
+ ),
142
+ )
143
+ self.hashed_password = 'pbkdf2:%s$%d$%s' % (
144
+ binascii.hexlify(salt).decode('ascii'),
145
+ ITERATIONS,
146
+ binascii.hexlify(h).decode('ascii'),
147
+ )
148
+ elif method == 'bcrypt':
149
+ import bcrypt
150
+ with tracer.start_as_current_span(
151
+ 'taguette/set_password',
152
+ attributes={'method': method},
153
+ ):
154
+ h = await asyncio.get_event_loop().run_in_executor(
155
+ None,
156
+ lambda: bcrypt.hashpw(password.encode('utf-8'),
157
+ bcrypt.gensalt()),
158
+ )
159
+ self.hashed_password = 'bcrypt:%s' % h.decode('utf-8')
160
+ else:
161
+ raise ValueError("Unsupported encryption method %r" % method)
162
+ self.password_set_date = datetime.utcnow()
163
+
164
+ async def check_password(self, password):
165
+ if self.hashed_password is None:
166
+ return False
167
+ if self.hashed_password.startswith('scrypt'):
168
+ with tracer.start_as_current_span(
169
+ 'taguette/check_password',
170
+ attributes={'method': 'scrypt'},
171
+ ):
172
+ pw = self.hashed_password[7:]
173
+ salt, n, r, p, hash_pw = pw.split('$', 4)
174
+ salt = binascii.unhexlify(salt.encode('ascii'))
175
+ n = int(n, 10)
176
+ r = int(r, 10)
177
+ p = int(p, 10)
178
+ hash_pw = binascii.unhexlify(hash_pw.encode('ascii'))
179
+ h = await asyncio.get_event_loop().run_in_executor(
180
+ None,
181
+ lambda: hashlib.scrypt(
182
+ password.encode('utf-8'),
183
+ salt=salt, n=n, r=r, p=p,
184
+ maxmem=SCRYPT_MAX_MEM,
185
+ ),
186
+ )
187
+ return hmac.compare_digest(
188
+ hash_pw,
189
+ h,
190
+ )
191
+ elif self.hashed_password.startswith('pbkdf2:'):
192
+ with tracer.start_as_current_span(
193
+ 'taguette/check_password',
194
+ attributes={'method': 'pbkdf2'},
195
+ ):
196
+ pw = self.hashed_password[7:]
197
+ salt, iterations, hash_pw = pw.split('$', 2)
198
+ salt = binascii.unhexlify(salt.encode('ascii'))
199
+ iterations = int(iterations, 10)
200
+ hash_pw = binascii.unhexlify(hash_pw.encode('ascii'))
201
+ h = await asyncio.get_event_loop().run_in_executor(
202
+ None,
203
+ lambda: hashlib.pbkdf2_hmac(
204
+ 'sha256', password.encode('utf-8'),
205
+ salt, iterations,
206
+ ),
207
+ )
208
+ return hmac.compare_digest(
209
+ hash_pw,
210
+ h,
211
+ )
212
+ elif self.hashed_password.startswith('bcrypt:'):
213
+ import bcrypt
214
+ with tracer.start_as_current_span(
215
+ 'taguette/check_password',
216
+ attributes={'method': 'bcrypt'},
217
+ ):
218
+ return await asyncio.get_event_loop().run_in_executor(
219
+ None,
220
+ lambda: bcrypt.checkpw(
221
+ password.encode('utf-8'),
222
+ self.hashed_password[7:].encode('utf-8'),
223
+ ),
224
+ )
225
+ else:
226
+ logger.warning("Password uses unknown encryption method")
227
+ return False
228
+
229
+ def __repr__(self):
230
+ return '<%s.%s %r>' % (
231
+ self.__class__.__module__,
232
+ self.__class__.__name__,
233
+ self.login,
234
+ )
235
+
236
+
237
+ # Table d'association many-to-many entre collections et documents
238
+ collection_documents = Table(
239
+ 'collection_documents',
240
+ Base.metadata,
241
+ Column('collection_id', Integer, ForeignKey(
242
+ 'collections.id', ondelete='CASCADE'), primary_key=True),
243
+ Column('document_id', Integer, ForeignKey(
244
+ 'documents.id', ondelete='CASCADE'), primary_key=True),
245
+ )
246
+
247
+
248
+ class Collection(Base):
249
+ __tablename__ = 'collections'
250
+ __table_args__ = ({'sqlite_autoincrement': True},)
251
+
252
+ id = Column(Integer, primary_key=True)
253
+ name = Column(String(200), nullable=False)
254
+ description = Column(Text, nullable=True)
255
+ created = Column(DateTime, nullable=False,
256
+ default=lambda: datetime.utcnow())
257
+ owner_login = Column(String(255), ForeignKey('users.login'), nullable=True)
258
+ owner = relationship('User')
259
+ project_id = Column(Integer, ForeignKey(
260
+ 'projects.id', ondelete='CASCADE'), nullable=False, index=True)
261
+ project = relationship('Project', back_populates='collections')
262
+
263
+ parent_id = Column(Integer,
264
+ ForeignKey('collections.id', ondelete='CASCADE'),
265
+ nullable=True, index=True)
266
+ parent = relationship('Collection', remote_side=[id])
267
+
268
+ documents = relationship(
269
+ 'Document',
270
+ secondary=collection_documents,
271
+ back_populates='collections'
272
+ )
273
+
274
+ def __repr__(self):
275
+ return f'<Collection {self.id} {self.name}>'
276
+
277
+
278
+ class Project(Base):
279
+ __tablename__ = 'projects'
280
+ __table_args__ = ({'sqlite_autoincrement': True},)
281
+
282
+ id = Column(Integer, primary_key=True)
283
+ name = Column(String(200), nullable=False)
284
+ description = Column(Text, nullable=False)
285
+ created = Column(DateTime, nullable=False,
286
+ default=lambda: datetime.utcnow())
287
+ is_archived = Column(Boolean, nullable=False, default=False)
288
+ compact_layout = Column(Boolean, nullable=True, default=False)
289
+ members = relationship('ProjectMember', cascade='all,delete-orphan',
290
+ back_populates='project')
291
+ commands = relationship('Command', cascade='all,delete-orphan',
292
+ passive_deletes=True,
293
+ back_populates='project')
294
+ documents = relationship('Document', cascade='all,delete-orphan',
295
+ passive_deletes=True)
296
+ tags = relationship('Tag', cascade='all,delete-orphan', order_by='Tag.id',
297
+ passive_deletes=True,
298
+ back_populates='project')
299
+ collections = relationship(
300
+ 'Collection', cascade='all,delete-orphan', back_populates='project')
301
+
302
+ def __repr__(self):
303
+ return '<%s.%s %r %r>' % (
304
+ self.__class__.__module__,
305
+ self.__class__.__name__,
306
+ self.id,
307
+ self.name,
308
+ )
309
+
310
+
311
+ class Privileges(enum.Enum):
312
+ ADMIN = 1
313
+ MANAGE_DOCS = 3
314
+ TAG = 2
315
+ READ = 0
316
+
317
+ # Admin operations
318
+ def can_edit_project_meta(self):
319
+ return self == Privileges.ADMIN
320
+
321
+ def can_delete_project(self):
322
+ return self == Privileges.ADMIN
323
+
324
+ def can_edit_members(self):
325
+ return self == Privileges.ADMIN
326
+
327
+ # Document operations
328
+ def can_edit_document(self):
329
+ return self in (Privileges.ADMIN, Privileges.MANAGE_DOCS)
330
+ can_delete_document = can_edit_document
331
+ can_add_document = can_edit_document
332
+
333
+ # Tagging operations
334
+ def can_update_tag(self):
335
+ return self in (Privileges.ADMIN, Privileges.MANAGE_DOCS,
336
+ Privileges.TAG)
337
+ can_add_tag = can_update_tag
338
+ can_delete_tag = can_update_tag
339
+ can_merge_tags = can_update_tag
340
+
341
+ def can_import_codebook(self):
342
+ return self == Privileges.ADMIN
343
+
344
+ def can_add_highlight(self):
345
+ return self in (Privileges.ADMIN, Privileges.MANAGE_DOCS,
346
+ Privileges.TAG)
347
+ can_delete_highlight = can_add_highlight
348
+
349
+
350
+ class ProjectMember(Base):
351
+ __tablename__ = 'project_members'
352
+
353
+ project_id = Column(Integer, ForeignKey('projects.id', ondelete='CASCADE'),
354
+ primary_key=True, index=True)
355
+ project = relationship('Project', back_populates='members')
356
+ user_login = Column(String(255),
357
+ ForeignKey('users.login',
358
+ ondelete='CASCADE', onupdate='CASCADE'),
359
+ primary_key=True, index=True)
360
+ user = relationship('User', back_populates='project_memberships')
361
+ privileges = Column(Enum(Privileges), nullable=False)
362
+
363
+ def __repr__(self):
364
+ return '<%s.%s project_id=%r user_login=%r>' % (
365
+ self.__class__.__module__,
366
+ self.__class__.__name__,
367
+ self.project_id,
368
+ self.user_login,
369
+ )
370
+
371
+
372
+ class TextDirection(enum.Enum):
373
+ LEFT_TO_RIGHT = 0
374
+ RIGHT_TO_LEFT = 1
375
+
376
+
377
+ class Document(Base):
378
+ __tablename__ = 'documents'
379
+ __table_args__ = ({'sqlite_autoincrement': True},)
380
+
381
+ id = Column(Integer, primary_key=True)
382
+ name = Column(String(200), nullable=False)
383
+ description = Column(Text, nullable=False)
384
+ filename = Column(String(200), nullable=False)
385
+ created = Column(DateTime, nullable=False,
386
+ default=lambda: datetime.utcnow())
387
+ project_id = Column(Integer, ForeignKey('projects.id', ondelete='CASCADE'),
388
+ nullable=False, index=True)
389
+ project = relationship('Project', back_populates='documents')
390
+ text_direction = Column(Enum(TextDirection), nullable=False)
391
+ contents = deferred(Column(Text, nullable=False))
392
+ highlights = relationship('Highlight', cascade='all,delete-orphan',
393
+ passive_deletes=True)
394
+ favorite = Column(Boolean, nullable=True, default=False)
395
+ collections = relationship(
396
+ 'Collection',
397
+ secondary=collection_documents,
398
+ back_populates='documents'
399
+ )
400
+
401
+ def __repr__(self):
402
+ return '<%s.%s %r %r project_id=%r>' % (
403
+ self.__class__.__module__,
404
+ self.__class__.__name__,
405
+ self.id,
406
+ self.name,
407
+ self.project_id,
408
+ )
409
+
410
+
411
+ def command_fields(columns, payload_fields):
412
+ def wrapper(func):
413
+ func.columns = columns
414
+ func.payload_fields = payload_fields
415
+ return func
416
+ return wrapper
417
+
418
+
419
+ class Command(Base):
420
+ __tablename__ = 'commands'
421
+ __table_args__ = ({'sqlite_autoincrement': True},)
422
+
423
+ id = Column(Integer, primary_key=True)
424
+ date = Column(DateTime, nullable=False,
425
+ default=lambda: datetime.utcnow(), index=True)
426
+ user_login = Column(String(255),
427
+ ForeignKey('users.login', onupdate='CASCADE'),
428
+ nullable=False, index=True)
429
+ user = relationship('User')
430
+ project_id = Column(Integer, ForeignKey('projects.id', ondelete='CASCADE'),
431
+ nullable=False, index=True)
432
+ project = relationship('Project', back_populates='commands')
433
+ document_id = Column(Integer, # Not ForeignKey, document can go away
434
+ nullable=True, index=True)
435
+ payload = Column(JSON, nullable=False)
436
+
437
+ tag_count_changes = None
438
+
439
+ __table_args__ = (
440
+ Index('idx_project_document', 'project_id', 'document_id'),
441
+ Index('idx_project_id', 'project_id', 'id'),
442
+ ) + __table_args__
443
+
444
+ def __init__(self, **kwargs):
445
+ if 'payload' in kwargs:
446
+ PROM_COMMAND.labels(kwargs['payload']['type']).inc()
447
+ super(Command, self).__init__(**kwargs)
448
+
449
+ def to_json(self):
450
+ cmd_json = dict(self.payload)
451
+ cmd_json['id'] = self.id
452
+ cmd_json['user_login'] = self.user_login
453
+ cmd_json['project_id'] = self.project_id
454
+ if self.document_id is not None:
455
+ cmd_json['document_id'] = self.document_id
456
+ if self.tag_count_changes is not None:
457
+ cmd_json['tag_count_changes'] = self.tag_count_changes
458
+ return cmd_json
459
+
460
+ TYPES = {'project_meta', 'document_add', 'document_delete',
461
+ 'highlight_add', 'highlight_delete', 'tag_add', 'tag_delete',
462
+ 'tag_merge', 'member_add', 'member_remove', 'project_import',
463
+ 'collection_add', 'collection_delete', 'collection_update'}
464
+
465
+ for n in TYPES:
466
+ PROM_COMMAND.labels(n).inc(0)
467
+
468
+ @classmethod
469
+ @command_fields(
470
+ columns=['project_id'],
471
+ payload_fields=['project_name', 'description'],
472
+ )
473
+ def project_meta(cls, user_login, project_id, name, description):
474
+ return cls(
475
+ user_login=user_login,
476
+ project_id=project_id,
477
+ payload={'type': 'project_meta', # keep in sync above
478
+ 'project_name': name,
479
+ 'description': description},
480
+ )
481
+
482
+ @classmethod
483
+ @command_fields(
484
+ columns=['project_id', 'document_id'],
485
+ payload_fields=['document_name',
486
+ 'description',
487
+ 'favorite',
488
+ 'text_direction',
489
+ 'collections'],
490
+ )
491
+ def document_add(cls, user_login, document):
492
+ assert isinstance(document.id, int)
493
+ return cls(
494
+ user_login=user_login,
495
+ project=document.project,
496
+ document_id=document.id,
497
+ payload={'type': 'document_add', # keep in sync above
498
+ 'document_name': document.name,
499
+ 'description': document.description,
500
+ 'favorite': document.favorite,
501
+ 'text_direction': document.text_direction.name,
502
+ 'collections': [c.id for c in document.collections]},
503
+ )
504
+
505
+ @classmethod
506
+ @command_fields(
507
+ columns=['project_id', 'document_id'],
508
+ payload_fields=[],
509
+ )
510
+ def document_delete(cls, user_login, document):
511
+ assert isinstance(document, Document)
512
+ assert isinstance(document.id, int)
513
+ return cls(
514
+ user_login=user_login,
515
+ project_id=document.project_id,
516
+ document_id=document.id,
517
+ payload={'type': 'document_delete'}, # keep in sync above
518
+ )
519
+
520
+ @classmethod
521
+ @command_fields(
522
+ columns=['project_id', 'document_id'],
523
+ payload_fields=[
524
+ 'highlight_id', 'start_offset', 'end_offset', 'tags', 'snippet',
525
+ ],
526
+ )
527
+ def highlight_add(cls, user_login, document, highlight, tags):
528
+ assert isinstance(highlight.id, int)
529
+ return cls(
530
+ user_login=user_login,
531
+ project_id=document.project_id,
532
+ document_id=document.id,
533
+ payload={'type': 'highlight_add', # keep in sync above
534
+ 'highlight_id': highlight.id,
535
+ 'start_offset': highlight.start_offset,
536
+ 'end_offset': highlight.end_offset,
537
+ 'snippet': highlight.snippet,
538
+ 'tags': tags},
539
+ )
540
+
541
+ @classmethod
542
+ @command_fields(
543
+ columns=['project_id', 'document_id'],
544
+ payload_fields=['highlight_id'],
545
+ )
546
+ def highlight_delete(cls, user_login, document, highlight_id):
547
+ assert isinstance(highlight_id, int)
548
+ return cls(
549
+ user_login=user_login,
550
+ project_id=document.project_id,
551
+ document_id=document.id,
552
+ payload={'type': 'highlight_delete', # keep in sync above
553
+ 'highlight_id': highlight_id},
554
+ )
555
+
556
+ @classmethod
557
+ @command_fields(
558
+ columns=['project_id'],
559
+ payload_fields=['tag_id',
560
+ 'tag_path',
561
+ 'tag_color',
562
+ 'tag_parent',
563
+ 'description'],
564
+ )
565
+ def tag_add(cls, user_login, tag):
566
+ assert isinstance(tag, Tag)
567
+ assert isinstance(tag.id, int)
568
+ return cls(
569
+ user_login=user_login,
570
+ project_id=tag.project_id,
571
+ payload={'type': 'tag_add', # keep in sync above
572
+ 'tag_id': tag.id,
573
+ 'tag_path': tag.path,
574
+ 'tag_color': tag.color,
575
+ 'tag_parent': tag.parent_id,
576
+ 'description': tag.description},
577
+ )
578
+
579
+ @classmethod
580
+ @command_fields(
581
+ columns=['project_id'],
582
+ payload_fields=['tag_id'],
583
+ )
584
+ def tag_delete(cls, user_login, project_id, tag_id):
585
+ assert isinstance(project_id, int)
586
+ assert isinstance(tag_id, int)
587
+ return cls(
588
+ user_login=user_login,
589
+ project_id=project_id,
590
+ payload={'type': 'tag_delete', # keep in sync above
591
+ 'tag_id': tag_id},
592
+ )
593
+
594
+ @classmethod
595
+ @command_fields(
596
+ columns=['project_id'],
597
+ payload_fields=['src_tag_id', 'dest_tag_id'],
598
+ )
599
+ def tag_merge(cls, user_login, project_id, tag_src, tag_dest):
600
+ assert isinstance(project_id, int)
601
+ assert isinstance(tag_src, int)
602
+ assert isinstance(tag_dest, int)
603
+ return cls(
604
+ user_login=user_login,
605
+ project_id=project_id,
606
+ payload={'type': 'tag_merge', # keep in sync above
607
+ 'src_tag_id': tag_src,
608
+ 'dest_tag_id': tag_dest},
609
+ )
610
+
611
+ @classmethod
612
+ @command_fields(
613
+ columns=['project_id'],
614
+ payload_fields=['member', 'privileges'],
615
+ )
616
+ def member_add(cls, user_login, project_id, member_login, privileges):
617
+ assert isinstance(project_id, int)
618
+ assert isinstance(privileges, Privileges)
619
+ return cls(
620
+ user_login=user_login,
621
+ project_id=project_id,
622
+ payload={'type': 'member_add', # keep in sync above
623
+ 'member': member_login,
624
+ 'privileges': privileges.name}
625
+ )
626
+
627
+ @classmethod
628
+ @command_fields(
629
+ columns=['project_id'],
630
+ payload_fields=['member'],
631
+ )
632
+ def member_remove(cls, user_login, project_id, member_login):
633
+ assert isinstance(project_id, int)
634
+ return cls(
635
+ user_login=user_login,
636
+ project_id=project_id,
637
+ payload={'type': 'member_remove', # keep in sync above
638
+ 'member': member_login}
639
+ )
640
+
641
+ @classmethod
642
+ @command_fields(
643
+ columns=['project_id'],
644
+ payload_fields=[],
645
+ )
646
+ def project_import(cls, user_login, project_id):
647
+ assert isinstance(project_id, int)
648
+ return cls(
649
+ user_login=user_login,
650
+ project_id=project_id,
651
+ payload={'type': 'project_import'} # keep in sync above
652
+ )
653
+
654
+ @classmethod
655
+ @command_fields(
656
+ columns=['project_id'],
657
+ payload_fields=['collection_id', 'collection_name', 'description',
658
+ 'parent_id', 'document_ids'],
659
+ )
660
+ def collection_add(cls, user_login, collection):
661
+ assert isinstance(collection.id, int)
662
+ return cls(
663
+ user_login=user_login,
664
+ project_id=collection.project_id,
665
+ payload={'type': 'collection_add', # keep in sync above
666
+ 'collection_id': collection.id,
667
+ 'collection_name': collection.name,
668
+ 'description': collection.description,
669
+ 'parent_id': collection.parent_id,
670
+ 'document_ids': [d.id for d in collection.documents]}
671
+ )
672
+
673
+ @classmethod
674
+ @command_fields(
675
+ columns=['project_id'],
676
+ payload_fields=['collection_id'],
677
+ )
678
+ def collection_delete(cls, user_login, collection):
679
+ assert isinstance(collection, Collection)
680
+ return cls(
681
+ user_login=user_login,
682
+ project_id=collection.project_id,
683
+ payload={'type': 'collection_delete', # keep in sync above
684
+ 'collection_id': collection.id}
685
+ )
686
+
687
+ @classmethod
688
+ @command_fields(
689
+ columns=['project_id'],
690
+ payload_fields=['collection_id',
691
+ 'collection_name',
692
+ 'description',
693
+ 'parent_id',
694
+ 'document_ids'],
695
+ )
696
+ def collection_update(cls, user_login, collection):
697
+ assert isinstance(collection, Collection)
698
+ return cls(
699
+ user_login=user_login,
700
+ project_id=collection.project_id,
701
+ payload={'type': 'collection_update', # keep in sync above
702
+ 'collection_id': collection.id,
703
+ 'collection_name': collection.name,
704
+ 'description': collection.description,
705
+ 'parent_id': collection.parent_id,
706
+ 'document_ids': [d.id for d in collection.documents]}
707
+ )
708
+
709
+ def __repr__(self):
710
+ return (
711
+ '<%s.%s %r %r user_login=%r project_id=%r document_id=%r type=%r>'
712
+ ) % (
713
+ self.__class__.__module__,
714
+ self.__class__.__name__,
715
+ self.id,
716
+ self.date,
717
+ self.user_login,
718
+ self.project_id,
719
+ self.document_id,
720
+ self.payload.get('type'),
721
+ )
722
+
723
+
724
+ Project.last_event = column_property(
725
+ select(
726
+ [Command.id]
727
+ ).where(
728
+ Command.project_id == Project.id
729
+ ).order_by(
730
+ Command.id.desc()
731
+ ).limit(1)
732
+ .as_scalar()
733
+ )
734
+
735
+
736
+ class Highlight(Base):
737
+ __tablename__ = 'highlights'
738
+ __table_args__ = ({'sqlite_autoincrement': True},)
739
+
740
+ id = Column(Integer, primary_key=True)
741
+ document_id = Column(Integer, ForeignKey('documents.id',
742
+ ondelete='CASCADE'),
743
+ nullable=False, index=True)
744
+ document = relationship('Document', back_populates='highlights')
745
+ start_offset = Column(Integer, nullable=False)
746
+ end_offset = Column(Integer, nullable=False)
747
+ snippet = Column(Text, nullable=False)
748
+ tags = relationship('Tag', secondary='highlight_tags',
749
+ back_populates='highlights')
750
+
751
+ def __repr__(self):
752
+ return '<%s.%s %r document_id=%r tags=[%s]>' % (
753
+ self.__class__.__module__,
754
+ self.__class__.__name__,
755
+ self.id,
756
+ self.document_id,
757
+ ' '.join(sorted(str(t.id) for t in self.tags)),
758
+ )
759
+
760
+
761
+ class Tag(Base):
762
+ __tablename__ = 'tags'
763
+ __table_args__ = ({'sqlite_autoincrement': True},)
764
+
765
+ id = Column(Integer, primary_key=True)
766
+ project_id = Column(Integer, ForeignKey('projects.id', ondelete='CASCADE'),
767
+ nullable=False, index=True)
768
+ project = relationship('Project', back_populates='tags')
769
+
770
+ path = Column(String(200), nullable=False, index=True)
771
+ description = Column(Text, nullable=False)
772
+ color = Column(String(20), nullable=False)
773
+
774
+ parent_id = Column(Integer, ForeignKey('tags.id', ondelete='CASCADE'),
775
+ nullable=True, index=True)
776
+ parent = relationship('Tag', remote_side=[id])
777
+
778
+ __table_args__ = (
779
+ UniqueConstraint('project_id', 'path'),
780
+ ) + __table_args__
781
+
782
+ highlights = relationship(
783
+ 'Highlight', secondary='highlight_tags',
784
+ back_populates='tags',
785
+ )
786
+
787
+ def __repr__(self):
788
+ return '<%s.%s %r %r project_id=%r>' % (
789
+ self.__class__.__module__,
790
+ self.__class__.__name__,
791
+ self.id,
792
+ self.path,
793
+ self.project_id,
794
+ )
795
+
796
+
797
+ highlight_tags = Table(
798
+ 'highlight_tags',
799
+ Base.metadata,
800
+ Column('highlight_id', ForeignKey('highlights.id', ondelete='CASCADE'),
801
+ primary_key=True, index=True),
802
+ Column('tag_id', ForeignKey('tags.id', ondelete='CASCADE'),
803
+ primary_key=True, index=True),
804
+ )
805
+
806
+ # Property to count highlight associate with tag
807
+ Tag.highlights_count = column_property(
808
+ select(
809
+ [functions.count(highlight_tags.c.highlight_id)],
810
+ )
811
+ .where(
812
+ highlight_tags.c.tag_id == Tag.id,
813
+ )
814
+ .correlate_except(highlight_tags)
815
+ .scalar_subquery()
816
+ )
817
+
818
+ # Property to get documents count associate with tag
819
+ Tag.documents_count = column_property(
820
+ select(
821
+ [functions.count(distinct(Document.id)).label('count')],
822
+ )
823
+ .where(
824
+ highlight_tags.c.tag_id == Tag.id,
825
+ highlight_tags.c.highlight_id == Highlight.id,
826
+ Highlight.document_id == Document.id,
827
+ )
828
+ .correlate_except(highlight_tags)
829
+ .scalar_subquery()
830
+ )
831
+
832
+ # Property to count highlights associated with a document
833
+ Document.highlights_count = column_property(
834
+ select(
835
+ [functions.count(Highlight.id)],
836
+ )
837
+ .where(
838
+ Highlight.document_id == Document.id,
839
+ )
840
+ .correlate_except(Highlight)
841
+ .scalar_subquery()
842
+ )
843
+
844
+ # Property to get ids documents associate with tag
845
+ document_alias = aliased(Document)
846
+
847
+
848
+ class GroupArray(functions.GenericFunction):
849
+ name = 'group_array'
850
+ type = ARRAY(Integer)
851
+
852
+
853
+ @compiles(GroupArray, 'postgresql')
854
+ def _compile_group_array_postgresql(element, compiler, **kw):
855
+ return "ARRAY_AGG(DISTINCT %s)" % compiler.process(element.clauses)
856
+
857
+
858
+ @compiles(GroupArray, 'sqlite')
859
+ def _compile_group_array_sqlite(element, compiler, **kw):
860
+ return "GROUP_CONCAT(DISTINCT %s)" % compiler.process(element.clauses)
861
+
862
+
863
+ @compiles(GroupArray, 'mysql')
864
+ def _compile_group_array_mysql(element, compiler, **kw):
865
+ return "GROUP_CONCAT(DISTINCT %s)" % compiler.process(element.clauses)
866
+
867
+
868
+ Tag.document_ids = column_property(
869
+ select([GroupArray(document_alias.id)])
870
+ .select_from(highlight_tags)
871
+ .outerjoin(Highlight, Highlight.id == highlight_tags.c.highlight_id)
872
+ .outerjoin(document_alias, document_alias.id == Highlight.document_id)
873
+ .where(highlight_tags.c.tag_id == Tag.id)
874
+ .correlate_except(highlight_tags)
875
+ .scalar_subquery()
876
+ )
877
+
878
+
879
+ # Property to count documents associate with projet
880
+ Project.document_count = column_property(
881
+ select([func.count(Document.id)])
882
+ .where(Document.project_id == Project.id)
883
+ .as_scalar()
884
+ )
885
+
886
+ # Property to count tags associate with projet
887
+ Project.tag_count = column_property(
888
+ select([func.count(Tag.id)])
889
+ .where(Tag.project_id == Project.id)
890
+ .as_scalar()
891
+ )
892
+
893
+ # Property to count members associate with projet
894
+ Project.member_count = column_property(
895
+ select([func.count(ProjectMember.user_login)])
896
+ .where(ProjectMember.project_id == Project.id)
897
+ .correlate(Project)
898
+ .scalar_subquery()
899
+ )
900
+
901
+
902
+ class EdugainIdps(Base):
903
+ __tablename__ = 'edigainidps'
904
+
905
+ id = Column(Integer, primary_key=True)
906
+ entityid = Column(String(255), nullable=True)
907
+ e_displayname = Column(Text(), nullable=True)
908
+ regauth = Column(String(255), nullable=True)
909
+ entity_cat = Column(String(255), nullable=True)
910
+ eccs_status = Column(Integer, nullable=True)
911
+ validator_status = Column(BigInteger(), nullable=True)
912
+ coco_status = Column(String(255), nullable=True)
913
+ coco_id = Column(Integer, nullable=True)
914
+ sirtfi_status = Column(Integer, nullable=True)
915
+ code = Column(String(255), nullable=True)
916
+ scopes = Column(Text(), nullable=True)
917
+
918
+ def __repr__(self):
919
+ return (f"<EdugainIdps {self.e_displayname} "
920
+ f"({self.id}, {self.entityid})>")