df_site 0.1.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.
- df_site/__init__.py +1 -0
- df_site/__main__.py +37 -0
- df_site/admin.py +130 -0
- df_site/apps.py +57 -0
- df_site/components/__init__.py +1 -0
- df_site/components/base.py +82 -0
- df_site/components/detail.py +191 -0
- df_site/components/list.py +446 -0
- df_site/components/list_filters.py +74 -0
- df_site/components/registry.py +55 -0
- df_site/constants.py +71 -0
- df_site/context_processors.py +61 -0
- df_site/defaults.py +319 -0
- df_site/dynamic_settings.py +37 -0
- df_site/form_fields.py +138 -0
- df_site/management/__init__.py +1 -0
- df_site/management/commands/__init__.py +1 -0
- df_site/management/commands/add_image.py +104 -0
- df_site/management/commands/generate_favicon.py +47 -0
- df_site/middleware.py +20 -0
- df_site/migrations/0001_initial.py +220 -0
- df_site/migrations/0002_alter_alertribbon_message_alter_alertribbon_summary.py +23 -0
- df_site/migrations/__init__.py +0 -0
- df_site/model_fields.py +35 -0
- df_site/models.py +130 -0
- df_site/postman/__init__.py +1 -0
- df_site/postman/forms.py +38 -0
- df_site/postman/urls.py +75 -0
- df_site/postman/views.py +65 -0
- df_site/static/css/app.css +0 -0
- df_site/static/css/base.css +22208 -0
- df_site/static/css/ckeditor5.css +422 -0
- df_site/static/favicon/android-chrome-192x192.png +0 -0
- df_site/static/favicon/android-chrome-512x512.png +0 -0
- df_site/static/favicon/apple-touch-icon.png +0 -0
- df_site/static/favicon/favicon-16x16.png +0 -0
- df_site/static/favicon/favicon-32x32.png +0 -0
- df_site/static/favicon/favicon.ico +0 -0
- df_site/static/favicon/mstile-150x150.png +0 -0
- df_site/static/favicon/safari-pinned-tab.svg +46 -0
- df_site/static/images/accessibility.svg +1 -0
- df_site/static/images/align-bottom.svg +1 -0
- df_site/static/images/align-center.svg +1 -0
- df_site/static/images/align-justify.svg +1 -0
- df_site/static/images/align-left.svg +1 -0
- df_site/static/images/align-middle.svg +1 -0
- df_site/static/images/align-right.svg +1 -0
- df_site/static/images/align-top.svg +1 -0
- df_site/static/images/bold.svg +1 -0
- df_site/static/images/browse-files.svg +1 -0
- df_site/static/images/bulletedlist.svg +1 -0
- df_site/static/images/cancel.svg +1 -0
- df_site/static/images/caption.svg +1 -0
- df_site/static/images/check.svg +1 -0
- df_site/static/images/code.svg +1 -0
- df_site/static/images/codeblock.svg +1 -0
- df_site/static/images/cog.svg +1 -0
- df_site/static/images/color-palette.svg +1 -0
- df_site/static/images/color-tile-check.svg +1 -0
- df_site/static/images/drag-handle.svg +1 -0
- df_site/static/images/drag-indicator.svg +1 -0
- df_site/static/images/dropdown-arrow.svg +1 -0
- df_site/static/images/eraser.svg +1 -0
- df_site/static/images/file-arrow-up-solid.svg +1 -0
- df_site/static/images/find-replace.svg +1 -0
- df_site/static/images/font-background.svg +1 -0
- df_site/static/images/font-color.svg +1 -0
- df_site/static/images/font-family.svg +1 -0
- df_site/static/images/font-size.svg +1 -0
- df_site/static/images/heading1.svg +1 -0
- df_site/static/images/heading2.svg +1 -0
- df_site/static/images/heading3.svg +1 -0
- df_site/static/images/heading4.svg +1 -0
- df_site/static/images/heading5.svg +1 -0
- df_site/static/images/heading6.svg +1 -0
- df_site/static/images/history.svg +1 -0
- df_site/static/images/horizontalline.svg +1 -0
- df_site/static/images/html.svg +1 -0
- df_site/static/images/image-asset-manager.svg +1 -0
- df_site/static/images/image-upload.svg +1 -0
- df_site/static/images/image-url.svg +1 -0
- df_site/static/images/image.svg +1 -0
- df_site/static/images/importexport.svg +1 -0
- df_site/static/images/indent.svg +1 -0
- df_site/static/images/italic.svg +1 -0
- df_site/static/images/link.svg +1 -0
- df_site/static/images/liststylecircle.svg +1 -0
- df_site/static/images/liststyledecimal.svg +1 -0
- df_site/static/images/liststyledecimalleadingzero.svg +1 -0
- df_site/static/images/liststyledisc.svg +1 -0
- df_site/static/images/liststylelowerlatin.svg +1 -0
- df_site/static/images/liststylelowerroman.svg +1 -0
- df_site/static/images/liststylesquare.svg +1 -0
- df_site/static/images/liststyleupperlatin.svg +1 -0
- df_site/static/images/liststyleupperroman.svg +1 -0
- df_site/static/images/loupe.svg +1 -0
- df_site/static/images/low-vision.svg +1 -0
- df_site/static/images/marker.svg +1 -0
- df_site/static/images/media-placeholder.svg +1 -0
- df_site/static/images/media.svg +1 -0
- df_site/static/images/next-arrow.svg +1 -0
- df_site/static/images/numberedlist.svg +1 -0
- df_site/static/images/object-center.svg +1 -0
- df_site/static/images/object-full-width.svg +1 -0
- df_site/static/images/object-inline-left.svg +1 -0
- df_site/static/images/object-inline-right.svg +1 -0
- df_site/static/images/object-inline.svg +1 -0
- df_site/static/images/object-left.svg +1 -0
- df_site/static/images/object-right.svg +1 -0
- df_site/static/images/object-size-custom.svg +1 -0
- df_site/static/images/object-size-full.svg +1 -0
- df_site/static/images/object-size-large.svg +1 -0
- df_site/static/images/object-size-medium.svg +1 -0
- df_site/static/images/object-size-small.svg +1 -0
- df_site/static/images/outdent.svg +1 -0
- df_site/static/images/paragraph.svg +1 -0
- df_site/static/images/pen.svg +1 -0
- df_site/static/images/pencil.svg +1 -0
- df_site/static/images/pilcrow.svg +1 -0
- df_site/static/images/plus.svg +1 -0
- df_site/static/images/previous-arrow.svg +1 -0
- df_site/static/images/project-logo.svg +1 -0
- df_site/static/images/quote.svg +1 -0
- df_site/static/images/redo.svg +1 -0
- df_site/static/images/remove-format.svg +1 -0
- df_site/static/images/return-arrow.svg +1 -0
- df_site/static/images/select-all.svg +1 -0
- df_site/static/images/show-blocks.svg +1 -0
- df_site/static/images/source-editing.svg +1 -0
- df_site/static/images/specialcharacters.svg +1 -0
- df_site/static/images/strikethrough.svg +1 -0
- df_site/static/images/subscript.svg +1 -0
- df_site/static/images/superscript.svg +1 -0
- df_site/static/images/table-cell-properties.svg +1 -0
- df_site/static/images/table-column.svg +1 -0
- df_site/static/images/table-merge-cell.svg +1 -0
- df_site/static/images/table-properties.svg +1 -0
- df_site/static/images/table-row.svg +1 -0
- df_site/static/images/table.svg +1 -0
- df_site/static/images/text-alternative.svg +1 -0
- df_site/static/images/text.svg +1 -0
- df_site/static/images/three-vertical-dots.svg +1 -0
- df_site/static/images/todolist.svg +1 -0
- df_site/static/images/underline.svg +1 -0
- df_site/static/images/undo.svg +1 -0
- df_site/static/images/unlink.svg +1 -0
- df_site/static/js/app.js +98 -0
- df_site/static/js/app.js.map +1 -0
- df_site/static/js/base.js +161181 -0
- df_site/static/js/base.js.map +1 -0
- df_site/static/translations/af.js +1 -0
- df_site/static/translations/ar.js +1 -0
- df_site/static/translations/ast.js +1 -0
- df_site/static/translations/az.js +1 -0
- df_site/static/translations/bg.js +1 -0
- df_site/static/translations/bn.js +1 -0
- df_site/static/translations/bs.js +1 -0
- df_site/static/translations/ca.js +1 -0
- df_site/static/translations/cs.js +1 -0
- df_site/static/translations/da.js +1 -0
- df_site/static/translations/de-ch.js +1 -0
- df_site/static/translations/de.js +1 -0
- df_site/static/translations/el.js +1 -0
- df_site/static/translations/en-au.js +1 -0
- df_site/static/translations/en-gb.js +1 -0
- df_site/static/translations/en.js +1 -0
- df_site/static/translations/eo.js +1 -0
- df_site/static/translations/es-co.js +1 -0
- df_site/static/translations/es.js +1 -0
- df_site/static/translations/et.js +1 -0
- df_site/static/translations/eu.js +1 -0
- df_site/static/translations/fa.js +1 -0
- df_site/static/translations/fi.js +1 -0
- df_site/static/translations/gl.js +1 -0
- df_site/static/translations/gu.js +1 -0
- df_site/static/translations/he.js +1 -0
- df_site/static/translations/hi.js +1 -0
- df_site/static/translations/hr.js +1 -0
- df_site/static/translations/hu.js +1 -0
- df_site/static/translations/hy.js +1 -0
- df_site/static/translations/id.js +1 -0
- df_site/static/translations/it.js +1 -0
- df_site/static/translations/ja.js +1 -0
- df_site/static/translations/jv.js +1 -0
- df_site/static/translations/kk.js +1 -0
- df_site/static/translations/km.js +1 -0
- df_site/static/translations/kn.js +1 -0
- df_site/static/translations/ko.js +1 -0
- df_site/static/translations/ku.js +1 -0
- df_site/static/translations/lt.js +1 -0
- df_site/static/translations/lv.js +1 -0
- df_site/static/translations/ms.js +1 -0
- df_site/static/translations/nb.js +1 -0
- df_site/static/translations/ne.js +1 -0
- df_site/static/translations/nl.js +1 -0
- df_site/static/translations/no.js +1 -0
- df_site/static/translations/oc.js +1 -0
- df_site/static/translations/pl.js +1 -0
- df_site/static/translations/pt-br.js +1 -0
- df_site/static/translations/pt.js +1 -0
- df_site/static/translations/ro.js +1 -0
- df_site/static/translations/ru.js +1 -0
- df_site/static/translations/si.js +1 -0
- df_site/static/translations/sk.js +1 -0
- df_site/static/translations/sl.js +1 -0
- df_site/static/translations/sq.js +1 -0
- df_site/static/translations/sr-latn.js +1 -0
- df_site/static/translations/sr.js +1 -0
- df_site/static/translations/sv.js +1 -0
- df_site/static/translations/th.js +1 -0
- df_site/static/translations/ti.js +1 -0
- df_site/static/translations/tk.js +1 -0
- df_site/static/translations/tr.js +1 -0
- df_site/static/translations/tt.js +1 -0
- df_site/static/translations/ug.js +1 -0
- df_site/static/translations/uk.js +1 -0
- df_site/static/translations/ur.js +1 -0
- df_site/static/translations/uz.js +1 -0
- df_site/static/translations/vi.js +1 -0
- df_site/static/translations/zh-cn.js +1 -0
- df_site/static/translations/zh.js +1 -0
- df_site/static/webfonts/fa-brands-400.ttf +0 -0
- df_site/static/webfonts/fa-brands-400.woff2 +0 -0
- df_site/static/webfonts/fa-regular-400.ttf +0 -0
- df_site/static/webfonts/fa-regular-400.woff2 +0 -0
- df_site/static/webfonts/fa-solid-900.ttf +0 -0
- df_site/static/webfonts/fa-solid-900.woff2 +0 -0
- df_site/static/webfonts/fa-v4compatibility.ttf +0 -0
- df_site/static/webfonts/fa-v4compatibility.woff2 +0 -0
- df_site/templates/account/email.html +78 -0
- df_site/templates/account/password_change.html +28 -0
- df_site/templates/account/snippets/warn_no_email.html +6 -0
- df_site/templates/allauth/elements/alert.html +6 -0
- df_site/templates/allauth/elements/badge.html +4 -0
- df_site/templates/allauth/elements/button.html +14 -0
- df_site/templates/allauth/elements/button_group.html +5 -0
- df_site/templates/allauth/elements/field.html +72 -0
- df_site/templates/allauth/elements/fields.html +3 -0
- df_site/templates/allauth/elements/form.html +10 -0
- df_site/templates/allauth/elements/h1.html +1 -0
- df_site/templates/allauth/elements/h2.html +1 -0
- df_site/templates/allauth/elements/img.html +4 -0
- df_site/templates/allauth/elements/p.html +1 -0
- df_site/templates/allauth/elements/panel.html +14 -0
- df_site/templates/allauth/elements/provider.html +6 -0
- df_site/templates/allauth/elements/provider_list.html +5 -0
- df_site/templates/allauth/elements/table.html +5 -0
- df_site/templates/allauth/layouts/base.html +14 -0
- df_site/templates/allauth/layouts/entrance.html +20 -0
- df_site/templates/allauth/layouts/manage.html +1 -0
- df_site/templates/cookie_consent/_cookie_group.html +64 -0
- df_site/templates/cookie_consent/cookiegroup_list.html +23 -0
- df_site/templates/df_components/base.html +0 -0
- df_site/templates/df_components/detail.html +12 -0
- df_site/templates/df_components/detail_fieldset.html +46 -0
- df_site/templates/df_components/list.html +42 -0
- df_site/templates/df_components/list_filter.html +13 -0
- df_site/templates/df_components/list_filters.html +36 -0
- df_site/templates/df_components/list_hierarchy.html +25 -0
- df_site/templates/df_components/list_pagination.html +39 -0
- df_site/templates/df_components/list_search_form.html +38 -0
- df_site/templates/df_components/list_table.html +35 -0
- df_site/templates/df_site/app.html +1 -0
- df_site/templates/df_site/base.html +221 -0
- df_site/templates/df_site/detail.html +8 -0
- df_site/templates/df_site/humans.txt +11 -0
- df_site/templates/df_site/manage_base.html +51 -0
- df_site/templates/df_site/popup_app.html +1 -0
- df_site/templates/df_site/popup_base.html +29 -0
- df_site/templates/df_site/security.txt +5 -0
- df_site/templates/django_bootstrap5/breadcrumb.html +17 -0
- df_site/templates/django_bootstrap5/pagination.html +40 -0
- df_site/templates/django_ckeditor_5/widget.html +13 -0
- df_site/templates/favicon/browserconfig.xml +9 -0
- df_site/templates/mfa/index.html +115 -0
- df_site/templates/mfa/recovery_codes/index.html +33 -0
- df_site/templates/mfa/webauthn/authenticator_list.html +74 -0
- df_site/templates/pipeline/css.html +1 -0
- df_site/templates/pipeline/js.html +1 -0
- df_site/templates/postman/archives.html +8 -0
- df_site/templates/postman/base.html +20 -0
- df_site/templates/postman/base_folder.html +71 -0
- df_site/templates/postman/base_write.html +26 -0
- df_site/templates/postman/inbox.html +7 -0
- df_site/templates/postman/inc_subject_ex.html +21 -0
- df_site/templates/postman/trash.html +12 -0
- df_site/templates/postman/view.html +64 -0
- df_site/templates/users/settings.html +26 -0
- df_site/templates/usersessions/usersession_list.html +70 -0
- df_site/templatetags/__init__.py +1 -0
- df_site/templatetags/df_site.py +241 -0
- df_site/templatetags/images.py +515 -0
- df_site/templatetags/pipeline_sri.py +97 -0
- df_site/testing/__init__.py +1 -0
- df_site/testing/multiple_views.py +369 -0
- df_site/testing/requests.py +299 -0
- df_site/urls.py +41 -0
- df_site/user_settings.py +69 -0
- df_site/users/__init__.py +1 -0
- df_site/users/forms.py +35 -0
- df_site/users/notifications.py +14 -0
- df_site/users/urls.py +17 -0
- df_site/users/views.py +75 -0
- df_site/views.py +122 -0
- df_site-0.1.0.dist-info/LICENSE +519 -0
- df_site-0.1.0.dist-info/METADATA +217 -0
- df_site-0.1.0.dist-info/RECORD +309 -0
- df_site-0.1.0.dist-info/WHEEL +4 -0
- df_site-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,446 @@
|
|
1
|
+
"""Components for displaying a list of items in a table."""
|
2
|
+
|
3
|
+
from typing import Callable, List, Optional, Tuple, Type, Union
|
4
|
+
|
5
|
+
from django.contrib.admin import ListFilter, ShowFacets, SimpleListFilter, widgets
|
6
|
+
from django.contrib.admin.options import IS_FACETS_VAR, IS_POPUP_VAR
|
7
|
+
from django.contrib.admin.templatetags.admin_list import date_hierarchy as raw_date_hierarchy
|
8
|
+
from django.contrib.admin.templatetags.admin_list import result_headers, results
|
9
|
+
from django.contrib.admin.utils import get_fields_from_path, lookup_spawns_duplicates
|
10
|
+
from django.contrib.admin.views.main import ALL_VAR, ORDER_VAR, PAGE_VAR, SEARCH_VAR, ChangeList
|
11
|
+
from django.core.exceptions import FieldDoesNotExist
|
12
|
+
from django.core.paginator import Paginator
|
13
|
+
from django.db import models
|
14
|
+
from django.db.models.constants import LOOKUP_SEP
|
15
|
+
from django.db.models.functions.text import Substr, Upper
|
16
|
+
from django.http import HttpRequest
|
17
|
+
from django.utils.http import urlencode
|
18
|
+
from django.utils.text import smart_split, unescape_string_literal
|
19
|
+
from django.utils.translation import gettext as _
|
20
|
+
|
21
|
+
from df_site.components.base import ModelComponent
|
22
|
+
|
23
|
+
|
24
|
+
class ModelListChangeList(ChangeList):
|
25
|
+
"""Customized ChangeList for ModelListComponent."""
|
26
|
+
|
27
|
+
formset = None
|
28
|
+
paginator: Paginator
|
29
|
+
|
30
|
+
def __init__(self, request: HttpRequest, *args, **kwargs):
|
31
|
+
"""Initialize the ChangeList."""
|
32
|
+
super().__init__(request, *args, **kwargs)
|
33
|
+
self.request: HttpRequest = request
|
34
|
+
|
35
|
+
def url_for_result(self, result: models.Model):
|
36
|
+
"""Return the URL for a result, trying to use the get_absolute_url method."""
|
37
|
+
if hasattr(result, "get_absolute_url"):
|
38
|
+
url = result.get_absolute_url()
|
39
|
+
else:
|
40
|
+
url = f"/{self.opts.app_label}/{self.opts.model_name}/{result.pk}/show/"
|
41
|
+
return url
|
42
|
+
|
43
|
+
|
44
|
+
class ModelListComponent(ModelComponent):
|
45
|
+
"""A component that can display a list as table, with filters and a search bar."""
|
46
|
+
|
47
|
+
page_var = PAGE_VAR
|
48
|
+
all_var = ALL_VAR
|
49
|
+
order_var = ORDER_VAR
|
50
|
+
search_var = SEARCH_VAR
|
51
|
+
changelist_filters_var = "_changelist_filters"
|
52
|
+
list_editable: List[str] = []
|
53
|
+
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
model: Type[models.Model],
|
57
|
+
base_template: str = "list.html",
|
58
|
+
list_select_related: Optional[List[str]] = None,
|
59
|
+
list_display: List[str] = None,
|
60
|
+
list_display_links: List[str] = None,
|
61
|
+
list_filter: List[Union[str, Tuple[str, Type[ListFilter]], Callable]] = None,
|
62
|
+
date_hierarchy: Optional[str] = None,
|
63
|
+
search_fields: List[str] = None,
|
64
|
+
list_per_page: int = 20,
|
65
|
+
list_max_show_all: int = 200,
|
66
|
+
sortable_by: List[str] = None,
|
67
|
+
search_help_text: Optional[str] = None,
|
68
|
+
show_facets: ShowFacets = ShowFacets.ALLOW,
|
69
|
+
show_full_result_count: bool = True,
|
70
|
+
ordering: List[str] = None,
|
71
|
+
pagination_on_top: bool = True,
|
72
|
+
pagination_on_bottom: bool = True,
|
73
|
+
filters_on_right: bool = True,
|
74
|
+
filters_title: str = _("Filters"),
|
75
|
+
):
|
76
|
+
"""Create a new list component."""
|
77
|
+
super().__init__(model, base_template)
|
78
|
+
self.list_select_related: Optional[List[str]] = list_select_related
|
79
|
+
self.list_display: List[str] = list_display or ["__str__"]
|
80
|
+
self.list_display_links: List[str] = list_display_links or []
|
81
|
+
self.list_filter: List[Union[str, Tuple[str, Type[ListFilter], Callable]]] = list_filter or []
|
82
|
+
self.date_hierarchy: Optional[str] = date_hierarchy
|
83
|
+
self.search_fields: List[str] = search_fields or []
|
84
|
+
self.list_per_page: int = list_per_page
|
85
|
+
self.list_max_show_all: int = list_max_show_all
|
86
|
+
self.sortable_by: List[str] = sortable_by or []
|
87
|
+
self.search_help_text: Optional[str] = search_help_text
|
88
|
+
self.show_facets: ShowFacets = show_facets
|
89
|
+
self.show_full_result_count: bool = show_full_result_count
|
90
|
+
self.ordering: List[str] = ordering or []
|
91
|
+
self.pagination_on_top: bool = pagination_on_top
|
92
|
+
self.pagination_on_bottom: bool = pagination_on_bottom
|
93
|
+
self.filters_on_right: bool = filters_on_right
|
94
|
+
self.filters_title: str = filters_title
|
95
|
+
|
96
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
97
|
+
def get_change_list_class(self, request: HttpRequest, **kwargs) -> Type[ChangeList]:
|
98
|
+
"""Return the ChangeList class to use for this component."""
|
99
|
+
return ModelListChangeList
|
100
|
+
|
101
|
+
# noinspection PyUnusedLocal
|
102
|
+
def get_change_list(self, request: HttpRequest, **kwargs) -> ChangeList:
|
103
|
+
"""Return the ChangeList instance to use for this component."""
|
104
|
+
cls = self.get_change_list_class(request, **kwargs)
|
105
|
+
return cls(
|
106
|
+
request,
|
107
|
+
self.model,
|
108
|
+
self.get_list_display(request, **kwargs),
|
109
|
+
self.get_list_display_links(request, **kwargs),
|
110
|
+
self.get_list_filter(request, **kwargs),
|
111
|
+
self.get_date_hierarchy(request, **kwargs),
|
112
|
+
self.get_search_fields(request, **kwargs),
|
113
|
+
self.list_select_related,
|
114
|
+
self.list_per_page,
|
115
|
+
self.list_max_show_all,
|
116
|
+
self.list_editable,
|
117
|
+
ModelAdminWrapper(self, kwargs),
|
118
|
+
self.sortable_by,
|
119
|
+
self.get_search_help_text(request, **kwargs),
|
120
|
+
)
|
121
|
+
|
122
|
+
# noinspection PyUnusedLocal
|
123
|
+
def get_date_hierarchy(self, request: HttpRequest, **kwargs):
|
124
|
+
"""Return the date hierarchy, depending on the request."""
|
125
|
+
return self.date_hierarchy
|
126
|
+
|
127
|
+
# noinspection PyUnusedLocal
|
128
|
+
def get_search_help_text(self, request: HttpRequest, **kwargs):
|
129
|
+
"""Return the text displayed before the search input."""
|
130
|
+
return self.search_help_text
|
131
|
+
|
132
|
+
# noinspection PyUnusedLocal
|
133
|
+
def get_list_display_links(self, request: HttpRequest, **kwargs):
|
134
|
+
"""Return the list of fields with the link to the object."""
|
135
|
+
return self.list_display_links
|
136
|
+
|
137
|
+
# noinspection PyUnusedLocal
|
138
|
+
def get_list_display(self, request: HttpRequest, **kwargs):
|
139
|
+
"""Return the list of fields to display."""
|
140
|
+
return self.list_display
|
141
|
+
|
142
|
+
def get_queryset(self, request: HttpRequest, **kwargs) -> models.QuerySet:
|
143
|
+
"""Return the queryset to use for this component."""
|
144
|
+
qs = self.get_base_queryset(request, **kwargs)
|
145
|
+
if self.list_select_related is True:
|
146
|
+
qs = qs.select_related()
|
147
|
+
elif self.list_select_related:
|
148
|
+
qs = qs.select_related(*self.list_select_related)
|
149
|
+
return qs
|
150
|
+
|
151
|
+
def get_preserved_filters(self, request: HttpRequest):
|
152
|
+
"""Return the preserved filters querystring."""
|
153
|
+
# noinspection PyArgumentList
|
154
|
+
preserved_filters = request.GET.urlencode()
|
155
|
+
if preserved_filters:
|
156
|
+
return urlencode({self.changelist_filters_var: preserved_filters})
|
157
|
+
return ""
|
158
|
+
|
159
|
+
# noinspection PyUnusedLocal
|
160
|
+
def to_field_allowed(self, request, to_field):
|
161
|
+
"""Mimic the behavior of the ModelAdmin."""
|
162
|
+
try:
|
163
|
+
field = self.opts.get_field(to_field)
|
164
|
+
except FieldDoesNotExist:
|
165
|
+
return False
|
166
|
+
|
167
|
+
return field.primary_key
|
168
|
+
|
169
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
170
|
+
def has_change_permission(self, request: HttpRequest, obj=None):
|
171
|
+
"""Mimic the behavior of the ModelAdmin."""
|
172
|
+
return False
|
173
|
+
|
174
|
+
def lookup_allowed(self, lookup, value, request: HttpRequest = None, **kwargs):
|
175
|
+
"""Mimic the behavior of the ModelAdmin."""
|
176
|
+
model = self.model
|
177
|
+
# Check FKey lookups that are allowed, so that popups produced by
|
178
|
+
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
|
179
|
+
# are allowed to work.
|
180
|
+
for fk_lookup in model._meta.related_fkey_lookups:
|
181
|
+
# As ``limit_choices_to`` can be a callable, invoke it here.
|
182
|
+
if callable(fk_lookup):
|
183
|
+
fk_lookup = fk_lookup()
|
184
|
+
if (lookup, value) in widgets.url_params_from_lookup_dict(fk_lookup).items():
|
185
|
+
return True
|
186
|
+
|
187
|
+
relation_parts = []
|
188
|
+
prev_field = None
|
189
|
+
part = ""
|
190
|
+
parts = lookup.split(LOOKUP_SEP)
|
191
|
+
for part in parts:
|
192
|
+
try:
|
193
|
+
field = model._meta.get_field(part)
|
194
|
+
except FieldDoesNotExist:
|
195
|
+
# Lookups on nonexistent fields are ok, since they're ignored
|
196
|
+
# later.
|
197
|
+
break
|
198
|
+
if not prev_field or (
|
199
|
+
prev_field.is_relation
|
200
|
+
and field not in model._meta.parents.values()
|
201
|
+
and field is not model._meta.auto_field
|
202
|
+
and (model._meta.auto_field is None or part not in getattr(prev_field, "to_fields", []))
|
203
|
+
and (field.is_relation or not field.primary_key)
|
204
|
+
):
|
205
|
+
relation_parts.append(part)
|
206
|
+
if not getattr(field, "path_infos", None):
|
207
|
+
# This is not a relational field, so further parts
|
208
|
+
# must be transforms.
|
209
|
+
break
|
210
|
+
prev_field = field
|
211
|
+
model = field.path_infos[-1].to_opts.model
|
212
|
+
|
213
|
+
if len(relation_parts) <= 1:
|
214
|
+
# Either a local field filter, or no fields at all.
|
215
|
+
return True
|
216
|
+
valid_lookups = {self.get_date_hierarchy(request, **kwargs)}
|
217
|
+
# RemovedInDjango60Warning: when the deprecation ends, replace with:
|
218
|
+
# for filter_item in self.get_list_filter(request):
|
219
|
+
list_filter = self.get_list_filter(request, **kwargs) if request is not None else self.list_filter
|
220
|
+
for filter_item in list_filter:
|
221
|
+
if isinstance(filter_item, type) and issubclass(filter_item, SimpleListFilter):
|
222
|
+
valid_lookups.add(filter_item.parameter_name)
|
223
|
+
elif isinstance(filter_item, (list, tuple)):
|
224
|
+
valid_lookups.add(filter_item[0])
|
225
|
+
else:
|
226
|
+
valid_lookups.add(filter_item)
|
227
|
+
|
228
|
+
# Is it a valid relational lookup?
|
229
|
+
return not {
|
230
|
+
LOOKUP_SEP.join(relation_parts),
|
231
|
+
LOOKUP_SEP.join(relation_parts + [part]),
|
232
|
+
}.isdisjoint(valid_lookups)
|
233
|
+
|
234
|
+
# noinspection PyUnusedLocal
|
235
|
+
def get_list_filter(self, request, **kwargs):
|
236
|
+
"""Return a sequence containing the fields to be displayed as filters."""
|
237
|
+
return self.list_filter
|
238
|
+
|
239
|
+
# noinspection PyUnusedLocal,PyMethodMayBeStatic
|
240
|
+
def get_paginator(self, request: HttpRequest, queryset, per_page, orphans=0, allow_empty_first_page=True):
|
241
|
+
"""Return the paginator to use for this component."""
|
242
|
+
return Paginator(queryset, per_page, orphans, allow_empty_first_page)
|
243
|
+
|
244
|
+
# noinspection PyUnusedLocal
|
245
|
+
def get_ordering(self, request: HttpRequest, **kwargs):
|
246
|
+
"""Return the ordering to use for this component."""
|
247
|
+
return self.ordering or ()
|
248
|
+
|
249
|
+
# noinspection PyUnusedLocal
|
250
|
+
def get_search_fields(self, request: HttpRequest, **kwargs):
|
251
|
+
"""Return a sequence containing the fields to be searched."""
|
252
|
+
return self.search_fields
|
253
|
+
|
254
|
+
def get_search_results(
|
255
|
+
self, request: HttpRequest, queryset: models.QuerySet, search_term: str
|
256
|
+
) -> Tuple[models.QuerySet, bool]:
|
257
|
+
"""Return the search result.
|
258
|
+
|
259
|
+
Return a tuple containing a queryset to implement the search
|
260
|
+
and a boolean indicating if the results may contain duplicates.
|
261
|
+
"""
|
262
|
+
|
263
|
+
# Apply keyword searches.
|
264
|
+
def construct_search(field_name):
|
265
|
+
if field_name.startswith("^"):
|
266
|
+
removeprefix = field_name.removeprefix("^")
|
267
|
+
return f"{removeprefix}__istartswith"
|
268
|
+
elif field_name.startswith("="):
|
269
|
+
removeprefix = field_name.removeprefix("=")
|
270
|
+
return f"{removeprefix}__iexact"
|
271
|
+
elif field_name.startswith("@"):
|
272
|
+
removeprefix = field_name.removeprefix("@")
|
273
|
+
return f"{removeprefix}__search"
|
274
|
+
# Use field_name if it includes a lookup.
|
275
|
+
opts = self.opts
|
276
|
+
lookup_fields = field_name.split(LOOKUP_SEP)
|
277
|
+
# Go through the fields, following all relations.
|
278
|
+
prev_field = None
|
279
|
+
for path_part in lookup_fields:
|
280
|
+
if path_part == "pk":
|
281
|
+
path_part = opts.pk.name
|
282
|
+
try:
|
283
|
+
field = opts.get_field(path_part)
|
284
|
+
except FieldDoesNotExist:
|
285
|
+
# Use valid query lookups.
|
286
|
+
if prev_field and prev_field.get_lookup(path_part):
|
287
|
+
return field_name
|
288
|
+
else:
|
289
|
+
prev_field = field
|
290
|
+
if hasattr(field, "path_infos"):
|
291
|
+
# Update opts to follow the relation.
|
292
|
+
opts = field.path_infos[-1].to_opts
|
293
|
+
# Otherwise, use the field with icontains.
|
294
|
+
return f"{field_name}__icontains"
|
295
|
+
|
296
|
+
may_have_duplicates = False
|
297
|
+
search_fields = self.get_search_fields(request)
|
298
|
+
if search_fields and search_term:
|
299
|
+
orm_lookups = [construct_search(str(search_field)) for search_field in search_fields]
|
300
|
+
term_queries = []
|
301
|
+
for bit in smart_split(search_term):
|
302
|
+
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
|
303
|
+
bit = unescape_string_literal(bit)
|
304
|
+
or_queries = models.Q.create(
|
305
|
+
[(orm_lookup, bit) for orm_lookup in orm_lookups],
|
306
|
+
connector=models.Q.OR,
|
307
|
+
)
|
308
|
+
term_queries.append(or_queries)
|
309
|
+
queryset = queryset.filter(models.Q.create(term_queries))
|
310
|
+
may_have_duplicates |= any(lookup_spawns_duplicates(self.opts, search_spec) for search_spec in orm_lookups)
|
311
|
+
return queryset, may_have_duplicates
|
312
|
+
|
313
|
+
def update_render_context(self, context, **kwargs):
|
314
|
+
"""Update the context before rendering the component."""
|
315
|
+
request = context["request"]
|
316
|
+
cl = self.get_change_list(request, **kwargs)
|
317
|
+
context.update({"cl": cl, "opts": self.opts})
|
318
|
+
context.update(self.get_pagination_context(request, cl, **kwargs))
|
319
|
+
context.update(self.get_search_context(request, cl, **kwargs))
|
320
|
+
context.update(self.get_hierarchy_context(request, cl, **kwargs))
|
321
|
+
|
322
|
+
# noinspection PyUnusedLocal
|
323
|
+
def get_pagination_context(self, request: HttpRequest, cl: ChangeList, **kwargs):
|
324
|
+
"""Return the context for the pagination."""
|
325
|
+
headers = list(result_headers(cl))
|
326
|
+
num_sorted_fields = 0
|
327
|
+
for h in headers:
|
328
|
+
if h["sortable"] and h["sorted"]:
|
329
|
+
num_sorted_fields += 1
|
330
|
+
pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page
|
331
|
+
page_range = cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else []
|
332
|
+
need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page
|
333
|
+
return {
|
334
|
+
"pagination_required": pagination_required,
|
335
|
+
"show_all_url": need_show_all_link and cl.get_query_string({self.all_var: ""}),
|
336
|
+
"page_range": list(page_range),
|
337
|
+
"ALL_VAR": self.all_var,
|
338
|
+
"1": 1,
|
339
|
+
"results": list(results(cl)),
|
340
|
+
"result_headers": headers,
|
341
|
+
"num_sorted_fields": num_sorted_fields,
|
342
|
+
}
|
343
|
+
|
344
|
+
# noinspection PyUnusedLocal
|
345
|
+
def get_search_context(self, request: HttpRequest, cl: ChangeList, **kwargs):
|
346
|
+
"""Return the context for the search bar."""
|
347
|
+
return {
|
348
|
+
"show_result_count": cl.result_count != cl.full_result_count,
|
349
|
+
"search_var": self.search_var,
|
350
|
+
"is_popup_var": IS_POPUP_VAR,
|
351
|
+
"is_facets_var": IS_FACETS_VAR,
|
352
|
+
}
|
353
|
+
|
354
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
355
|
+
def get_hierarchy_context(self, request: HttpRequest, cl: ChangeList, **kwargs):
|
356
|
+
"""Return the context for the hierarchy of results.
|
357
|
+
|
358
|
+
Currently only works for date fields.
|
359
|
+
"""
|
360
|
+
# noinspection PyTestUnpassedFixture
|
361
|
+
field_name = cl.date_hierarchy
|
362
|
+
if not field_name:
|
363
|
+
return {"show_hierarchy": False}
|
364
|
+
field = get_fields_from_path(cl.model, field_name)[-1]
|
365
|
+
if isinstance(field, models.CharField) or isinstance(field, models.TextField):
|
366
|
+
hierarchy_data = first_letter_hierarchy(cl)
|
367
|
+
else:
|
368
|
+
hierarchy_data = raw_date_hierarchy(cl)
|
369
|
+
if hierarchy_data is not None:
|
370
|
+
return {
|
371
|
+
"show_hierarchy": True,
|
372
|
+
"hierarchy_back": hierarchy_data["back"],
|
373
|
+
"hierarchy_choices": hierarchy_data["choices"],
|
374
|
+
"hierarchy_title": field.verbose_name,
|
375
|
+
}
|
376
|
+
return {"show_hierarchy": False}
|
377
|
+
|
378
|
+
|
379
|
+
class ModelAdminWrapper:
|
380
|
+
"""Wrapper around a ModelListComponent to mimic a ModelAdmin.
|
381
|
+
|
382
|
+
This wrapper allows to add kwargs to the get_queryset method.
|
383
|
+
ChangeList expects a ModelAdmin instance but calls the get_queryset method with
|
384
|
+
only request arg, so we need to wrap the ModelListComponent.
|
385
|
+
"""
|
386
|
+
|
387
|
+
def __init__(self, model_admin: ModelListComponent, kwargs):
|
388
|
+
"""Create a new wrapper."""
|
389
|
+
self.model_admin: ModelListComponent = model_admin
|
390
|
+
self.kwargs = kwargs
|
391
|
+
|
392
|
+
def get_queryset(self, request):
|
393
|
+
"""Call the get_queryset method with kwargs."""
|
394
|
+
return self.model_admin.get_queryset(request, **self.kwargs)
|
395
|
+
|
396
|
+
def get_ordering(self, request):
|
397
|
+
"""Call the get_ordering method with kwargs."""
|
398
|
+
return self.model_admin.get_ordering(request, **self.kwargs)
|
399
|
+
|
400
|
+
def get_list_filter(self, request):
|
401
|
+
"""Call the get_list_filter method with kwargs."""
|
402
|
+
return self.model_admin.get_list_filter(request, **self.kwargs)
|
403
|
+
|
404
|
+
def get_date_hierarchy(self, request):
|
405
|
+
"""Call the get_date_hierarchy method with kwargs."""
|
406
|
+
return self.model_admin.get_date_hierarchy(request, **self.kwargs)
|
407
|
+
|
408
|
+
def lookup_allowed(self, lookup, value, request: HttpRequest):
|
409
|
+
"""Call the lookup_allowed method with kwargs."""
|
410
|
+
return self.model_admin.lookup_allowed(lookup, value, request=request, **self.kwargs)
|
411
|
+
|
412
|
+
def __getattr__(self, item):
|
413
|
+
"""Delegate all other calls to the ModelListComponent."""
|
414
|
+
return getattr(self.model_admin, item)
|
415
|
+
|
416
|
+
|
417
|
+
def first_letter_hierarchy(cl: ChangeList):
|
418
|
+
"""Fetch all initials of a CharField."""
|
419
|
+
# noinspection PyTestUnpassedFixture
|
420
|
+
qs = (
|
421
|
+
cl.queryset.annotate(hierarchy_initial=Upper(Substr(cl.date_hierarchy, 1, 1)))
|
422
|
+
.values_list("hierarchy_initial", flat=True)
|
423
|
+
.distinct()
|
424
|
+
)
|
425
|
+
values = set(qs)
|
426
|
+
if not values:
|
427
|
+
return None
|
428
|
+
# noinspection PyTestUnpassedFixture
|
429
|
+
initial_field = f"{cl.date_hierarchy}__istartswith"
|
430
|
+
initial_value = cl.params.get(initial_field)
|
431
|
+
if initial_value:
|
432
|
+
cl.get_query_string(remove=[initial_field])
|
433
|
+
back = {"link": cl.get_query_string(remove=[initial_field]), "title": _("All")}
|
434
|
+
else:
|
435
|
+
back = None
|
436
|
+
return {
|
437
|
+
"show": True,
|
438
|
+
"back": back,
|
439
|
+
"choices": [
|
440
|
+
{
|
441
|
+
"title": initial,
|
442
|
+
"link": cl.get_query_string({initial_field: initial}) if initial != initial_value else None,
|
443
|
+
}
|
444
|
+
for initial in sorted(values)
|
445
|
+
],
|
446
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
"""Override default admin filters with a custom template."""
|
2
|
+
|
3
|
+
from django.contrib.admin import AllValuesFieldListFilter as AllValuesFieldListFilterBase
|
4
|
+
from django.contrib.admin import BooleanFieldListFilter as BooleanFieldListFilterBase
|
5
|
+
from django.contrib.admin import ChoicesFieldListFilter as ChoicesFieldListFilterBase
|
6
|
+
from django.contrib.admin import DateFieldListFilter as DateFieldListFilterBase
|
7
|
+
from django.contrib.admin import EmptyFieldListFilter as EmptyFieldListFilterBase
|
8
|
+
from django.contrib.admin import FieldListFilter as FieldListFilterBase
|
9
|
+
from django.contrib.admin import RelatedFieldListFilter as RelatedFieldListFilterBase
|
10
|
+
from django.contrib.admin import RelatedOnlyFieldListFilter as RelatedOnlyFieldListFilterBase
|
11
|
+
from django.contrib.admin import SimpleListFilter as SimpleListFilterBase
|
12
|
+
|
13
|
+
|
14
|
+
class ListFilterMixin:
|
15
|
+
"""A mixin for list filters, replacing the default template."""
|
16
|
+
|
17
|
+
template = "df_components/list_filter.html"
|
18
|
+
|
19
|
+
|
20
|
+
class SimpleListFilter(ListFilterMixin, SimpleListFilterBase):
|
21
|
+
"""A filter for simple fields."""
|
22
|
+
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
class FieldListFilter(ListFilterMixin, FieldListFilterBase):
|
27
|
+
"""A filter for list fields."""
|
28
|
+
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
class RelatedFieldListFilter(ListFilterMixin, RelatedFieldListFilterBase):
|
33
|
+
"""A filter for related fields."""
|
34
|
+
|
35
|
+
def field_admin_ordering(self, field, request, model_admin):
|
36
|
+
"""Return the default ordering for related field, skipping the use of the admin site."""
|
37
|
+
# noinspection PyProtectedMember
|
38
|
+
return field.remote_field.model._meta.ordering
|
39
|
+
|
40
|
+
|
41
|
+
class BooleanFieldListFilter(ListFilterMixin, BooleanFieldListFilterBase):
|
42
|
+
"""A filter for boolean fields."""
|
43
|
+
|
44
|
+
pass
|
45
|
+
|
46
|
+
|
47
|
+
class ChoicesFieldListFilter(ListFilterMixin, ChoicesFieldListFilterBase):
|
48
|
+
"""A filter for choice fields."""
|
49
|
+
|
50
|
+
pass
|
51
|
+
|
52
|
+
|
53
|
+
class DateFieldListFilter(ListFilterMixin, DateFieldListFilterBase):
|
54
|
+
"""A filter for date fields."""
|
55
|
+
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
class AllValuesFieldListFilter(ListFilterMixin, AllValuesFieldListFilterBase):
|
60
|
+
"""A filter for all values of a field."""
|
61
|
+
|
62
|
+
pass
|
63
|
+
|
64
|
+
|
65
|
+
class RelatedOnlyFieldListFilter(ListFilterMixin, RelatedOnlyFieldListFilterBase):
|
66
|
+
"""A filter for related fields."""
|
67
|
+
|
68
|
+
pass
|
69
|
+
|
70
|
+
|
71
|
+
class EmptyFieldListFilter(ListFilterMixin, EmptyFieldListFilterBase):
|
72
|
+
"""A filter for empty fields."""
|
73
|
+
|
74
|
+
pass
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"""Simulate a classical Django admin_site with similare URLs."""
|
2
|
+
|
3
|
+
import warnings
|
4
|
+
from collections import defaultdict
|
5
|
+
from typing import Optional, Type
|
6
|
+
|
7
|
+
from django.contrib.admin import ModelAdmin
|
8
|
+
from django.db import models
|
9
|
+
|
10
|
+
|
11
|
+
class ModelURLRegistry:
|
12
|
+
"""A registry for models and their URLs."""
|
13
|
+
|
14
|
+
def __init__(self):
|
15
|
+
"""Initialize the registry."""
|
16
|
+
self.all_models = defaultdict(dict)
|
17
|
+
|
18
|
+
def register_model(self, app_label, model):
|
19
|
+
"""Register a model in the registry."""
|
20
|
+
# Since this method is called when models are imported, it cannot
|
21
|
+
# perform imports because of the risk of import loops. It mustn't
|
22
|
+
# call get_app_config().
|
23
|
+
# noinspection PyProtectedMember
|
24
|
+
model_name = model._meta.model_name
|
25
|
+
app_models = self.all_models[app_label]
|
26
|
+
if model_name in app_models:
|
27
|
+
existing = app_models[model_name]
|
28
|
+
if model.__name__ == existing.__name__ and model.__module__ == existing.__module__:
|
29
|
+
warnings.warn(
|
30
|
+
f"Model '{app_label}.{model_name}' was already registered. Reloading models is not "
|
31
|
+
"advised as it can lead to inconsistencies, most notably with "
|
32
|
+
"related models.",
|
33
|
+
RuntimeWarning,
|
34
|
+
stacklevel=2,
|
35
|
+
)
|
36
|
+
else:
|
37
|
+
msg = f"Conflicting '{model_name}' models in application '{app_label}': {existing} and {model}."
|
38
|
+
raise RuntimeError(msg)
|
39
|
+
app_models[model_name] = model
|
40
|
+
|
41
|
+
def unregister_model(self, app_label, model_name):
|
42
|
+
"""Unregister a model from the registry."""
|
43
|
+
if app_label in self.all_models:
|
44
|
+
if model_name in self.all_models[app_label]:
|
45
|
+
del self.all_models[app_label][model_name]
|
46
|
+
if len(self.all_models[app_label]) == 0:
|
47
|
+
del self.all_models[app_label]
|
48
|
+
|
49
|
+
# noinspection PyMethodMayBeStatic
|
50
|
+
def get_model_admin(self, model: Type[models.Model]) -> Optional[ModelAdmin]:
|
51
|
+
"""Return an empty model admin, for the sake of compatibility."""
|
52
|
+
return None
|
53
|
+
|
54
|
+
|
55
|
+
default_registry = ModelURLRegistry()
|
df_site/constants.py
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
"""Contains constants for the users app."""
|
2
|
+
|
3
|
+
import re
|
4
|
+
|
5
|
+
BRAND_ICONS = {
|
6
|
+
"amazon": "amazon",
|
7
|
+
"amazon_cognito": "amazon",
|
8
|
+
"angellist": "angellist",
|
9
|
+
"apple": "apple",
|
10
|
+
"atlassian": "atlassian",
|
11
|
+
"battlenet": "battle-net",
|
12
|
+
"bitbucket_oauth2": "bitbucket",
|
13
|
+
"digitalocean": "digital-ocean",
|
14
|
+
"discord": "discord",
|
15
|
+
"dropbox": "dropbox",
|
16
|
+
"evernote": "evernote",
|
17
|
+
"facebook": "facebook",
|
18
|
+
"figma": "figma",
|
19
|
+
"fivehundredpx": "500px",
|
20
|
+
"flickr": "flickr",
|
21
|
+
"foursquare": "foursquare",
|
22
|
+
"github": "github",
|
23
|
+
"gitlab": "gitlab",
|
24
|
+
"google": "google",
|
25
|
+
"hubspot": "hubspot",
|
26
|
+
"instagram": "instagram",
|
27
|
+
"line": "line",
|
28
|
+
"linkedin_oauth2": "linkedin",
|
29
|
+
"mailchimp": "mailchimp",
|
30
|
+
"mailru": "",
|
31
|
+
"meetup": "meetup",
|
32
|
+
"microsoft": "microsoft",
|
33
|
+
"odnoklassniki": "odnoklassniki",
|
34
|
+
"openid": "openid",
|
35
|
+
"openid_connect": "openid",
|
36
|
+
"orcid": "orcid",
|
37
|
+
"patreon": "patreon",
|
38
|
+
"paypal": "paypal",
|
39
|
+
"pinterest": "pinterest",
|
40
|
+
"pocket": "get-pocket",
|
41
|
+
"reddit": "reddit",
|
42
|
+
"salesforce": "salesforce",
|
43
|
+
"shopify": "shopify",
|
44
|
+
"slack": "slack",
|
45
|
+
"snapchat": "snapchat",
|
46
|
+
"soundcloud": "soundcloud",
|
47
|
+
"spotify": "spotify",
|
48
|
+
"stackexchange": "stack-exchange",
|
49
|
+
"steam": "steam",
|
50
|
+
"strava": "strava",
|
51
|
+
"stripe": "stripe",
|
52
|
+
"telegram": "telegram",
|
53
|
+
"tiktok": "tiktok",
|
54
|
+
"trello": "trello",
|
55
|
+
"tumblr": "tumblr",
|
56
|
+
"twitch": "twitch",
|
57
|
+
"twitter": "twitter",
|
58
|
+
"twitter_oauth2": "twitter",
|
59
|
+
"untappd": "untappd",
|
60
|
+
"vimeo": "vimeo",
|
61
|
+
"vimeo_oauth2": "vimeo",
|
62
|
+
"vk": "vk",
|
63
|
+
"weibo": "weibo",
|
64
|
+
"weixin": "weixin",
|
65
|
+
"windowslive": "windows",
|
66
|
+
"xing": "xing",
|
67
|
+
"yahoo": "yahoo",
|
68
|
+
"yandex": "yandex",
|
69
|
+
}
|
70
|
+
INT_RE = re.compile(r"^(0|[1-9]\d*)$")
|
71
|
+
SIZE_RE = re.compile(r"^([1-9]\d*)x([1-9]\d*)$")
|