# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['bookmarks',
 'bookmarks.migrations',
 'bookmarks.tests',
 'examples',
 'examples.migrations']

package_data = \
{'': ['*'],
 'bookmarks': ['static/css/*',
               'static/img/favicons/*',
               'templates/*',
               'templates/base_template/*',
               'templates/bookmarks/*',
               'templates/commons/*',
               'templates/tags/*'],
 'examples': ['fixtures/*']}

install_requires = \
['Django>=4.0,<5.0',
 'django-allauth>=0.47.0,<0.48.0',
 'django-debug-toolbar>=3.2.2,<4.0.0',
 'django-extensions>=3.1.5,<4.0.0']

setup_kwargs = {
    'name': 'django-yabl',
    'version': '0.0.1',
    'description': 'Yet another bookmarking library (yabl) for Django. Bookmark and tag arbitrary models.',
    'long_description': '# django-yabl\n\nYet another bookmarking library (yabl) for Django. Bookmark and tag arbitrary models.\n\n## Overview\n\nThe `AbstractBookmarkable` contains a `bookmarks` field mapped to a generic `Bookmark` model containing:\n\n1. the authenticated user adding the bookmark, i.e. the `bookmarker`;\n2. the concrete model instance referenced, i.e. the _bookmarked_;[^1] and\n3. a ManyToMany `tags` field which maps to a `TagItem` model.\n\n[^1]: The model is referenced via a `content_type` and an `object_id`\n\nThis abstraction makes each instance _bookmarkable_ and _taggable_ by users.\n\nSpecifically the following attributes are enabled:\n\n| Attribute                         | Purpose                                                    |\n| --------------------------------- | ---------------------------------------------------------- |\n| `is_bookmarked`(user)             | Check whether object instance is bookmarked or not         |\n| `get_bookmarked`(user)            | Get instances of model that user has bookmarked            |\n| `get_user_tags`(user)             | If user bookmarked, get user-made tags on instance         |\n| `toggle_bookmark`(user)           | Toggle bookmark status as bookmarked or not                |\n| `add_tags`(user, tags: list[str]) | Add unique tags, accepts a list of names                   |\n| `remove_tag`(user, tag: str)      | Delete an existing tag name from tags previously added     |\n| `set_bookmarked_context`(user)    | Combines relevant urls and attributes for template output  |\n| @`modal`                          | Custom modal enables: _toggle_, _add tags_, _remove tag_   |\n| @`launch_modal_url`               | URL to launch custom modal                                 |\n| @`add_tags_url`                   | URL to POST tags added                                     |\n| @`del_tag_url`                    | URL to DELETE tag added                                    |\n| @`toggle_status_url`              | URL to toggle bookmark status of an object instance added  |\n| @`object_content_for_panel`       | Content when custom modal is loaded; **must** be overriden |\n\n## Pre-requisites\n\n1. `django-allauth` setup\n2. `htmx` basics\n3. Overriding Django templates\n\n## Initial setup\n\n### Install in virtual environment\n\n```zsh\n.venv> pip3 install django-allauth # for authentication purposes only\n.venv> pip3 install django-bookmark-and-tag # poetry add django-bookmark-and-tag\n```\n\n### Add to project settings\n\n```python\n# settings.py\nINSTALLED_APPS = [\n    ...\n    # for authentication purposes only\n    "allauth",\n    "allauth.account",\n    "allauth.socialaccount",\n    # add the following:\n    "bookmarks", # new\n    "examples" # your app that will be bookmarked / tagged\n    ...\n]\n```\n\n### Setup django-allauth\n\nA basic configuration just to enable easy access to authentication urls / views / templates:\n\n```python\nAUTHENTICATION_BACKENDS = [\n    "django.contrib.auth.backends.ModelBackend",\n    "allauth.account.auth_backends.AuthenticationBackend",\n]\nSITE_ID = 1\nEMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"\nACCOUNT_LOGOUT_ON_GET = True  # no need to confirm logout\nACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False\nACCOUNT_EMAIL_VERIFICATION = "none"\nLOGIN_REDIRECT_URL = "/"\n```\n\nAdd `templates` root directory:\n\n```python\nTEMPLATES = [\n    {\n        ...\n        "DIRS": [BASE_DIR / "templates"],  # added\n        ...\n    }\n```\n\nCreate a file under this specific directory to override allauth defaults:\n\n```jinja\n<!-- templates/account/base.html -->\n{% extends \'base.html\' %}\n{% block title %} Authentication | BrandX {% endblock title %}\n\n<!-- This overrides allauth /templates base.html -->\n\n{% block content %}\n{% endblock content %}\n```\n\n### Add bookmarks urls\n\n```python\n# urls.py\nurlpatterns = [\n    ...\n    path("accounts/", include("allauth.urls")), # for ensuring authentication only\n    path("bookmarks/", include("bookmarks.urls")), # new\n    path("", include("examples.urls")), # \'examples.urls\' is just for purposes of testing this ought to be replaced by the app whose model will be bookmarked / tagged\n]\n```\n\n### Run migration\n\n```zsh\n.venv> python manage.py migrate\n```\n\n### Run tests\n\n```zsh\n.venv> pytest --ds=config.settings --cov\n```\n\n### Optional fixtures\n\nSample fixtures can be loaded into the `SampleBook` and `SampleQuote` model found in examples/models.py:\n\n```zsh\n>>> python manage.py loaddata bookquotes.yaml # fixtures which show sample books\n```\n\n## Backend Configuration\n\nLet\'s assume an `examples` app containing a `SampleBook` model:\n\n### Configure models.py\n\n#### Add mixin to target model\n\nAdd an `AbstractBookmarkable` mixin to the models.py:\n\n```python\n# examples/models.py\nclass SampleBook(AbstractBookmarkable):\n    ...\n```\n\n#### Add verbose name for contenttypes\n\nAdd a `verbose_name` to `Meta.options` so that if referenced via its `Content_Type` _.name_, the assigned `verbose_name` appears.\n\n```python\n# examples/models.py\nclass SampleBook(AbstractBookmarkable):\n    ...\n    class Meta:\n        verbose_name = "Book"  # see generic relations, e.g. content_type.name\n        verbose_name_plural = "Books"\n```\n\n#### Add mixin for attributes\n\nEach book instance `obj` has access to `@`properties and methods requiring a `user`:\n\n#### Customize display of model in panel\n\nMust declare an @`object_content_for_panel` in the inheriting model.\n\nEach display will be different because each model will have different fields / different html markup. Instead of rendering a separate template per model, can customize a model attribute via `format_html()`:\n\n```python\n# examples/models.py\nclass SampleBook(AbstractBookmarkable):\n    ...\n    @property\n    def object_content_for_panel(self) -> SafeText: # customizes appearance of a specific book within the custom modal\n        return format_html(\n            """\n            <h2>{title}</h2>\n            <h3>{author}</h3>\n            <p>{excerpt}</p>\n            """,\n            title=self.title,\n            excerpt=self.excerpt,\n            author=f"{self.author.first_name} {self.author.last_name}",\n        )\n```\n\n### Configure views.py (copy/paste)\n\nCopy and paste a preconfigured set of views, matching the same to another model, e.g. `ArbitraryPainting`. See sample set of views using a `SampleBook` model:\n\n```python\n# examples/views.py\nfrom bookmarks.utils import Pathmaker, MODAL_BASE, PANEL\n\n@login_required\n@require_GET\ndef launch_modal_samplebook(request: HttpRequest, pk: str) -> TemplateResponse: # change view name\n    obj = get_object_or_404(SampleBook, pk=pk) # change model to ArbitraryPainting\n    panel = {"content_template": PANEL}\n    context = obj.set_bookmarked_context(request.user) | panel\n    return TemplateResponse(request, MODAL_BASE, context)\n\n\n@login_required\n@require_http_methods(["PUT"])\ndef toggle_status_samplebook(\n    request: HttpRequest, pk: str\n) -> TemplateResponse: # change view name\n    obj = get_object_or_404(SampleBook, pk=pk) # change model to ArbitraryPainting\n    obj.toggle_bookmark(request.user)\n    context = obj.set_bookmarked_context(request.user)\n    return TemplateResponse(request, PANEL, context)\n\n\n@login_required\n@require_POST\ndef add_tags_samplebook(request: HttpRequest, pk: str) -> TemplateResponse: # change view name\n    obj = get_object_or_404(SampleBook, pk=pk) # change model to ArbitraryPainting\n    if submitted := request.POST.get("tags"):\n        if add_these := submitted.split(","):\n            obj.add_tags(request.user, add_these)\n    context = obj.set_bookmarked_context(request.user)\n    return TemplateResponse(request, PANEL, context)\n\n\n@login_required\n@require_http_methods(["DELETE"])\ndef del_tag_samplebook(request: HttpRequest, pk: str) -> HttpResponse: # change view name\n    obj = get_object_or_404(SampleBook, pk=pk) # change model to ArbitraryPainting\n    if delete_this := request.POST.get("tag"):\n        obj.remove_tag(request.user, delete_this)\n    return HttpResponse(headers={"HX-Trigger": "tagDeleted"})\n\n"""\nEach view function from app/`views.py` related to a particular model should be imported into app/`urls.py`. The views can be consolidated to a single pattern based on a `Pathmaker` helper dataclass defined in bookmark/utils.py.\n"""\nBOOK = Pathmaker(\n    model_klass=SampleBook, # change model to ArbitraryPainting\n    launch_func=launch_modal_samplebook, # add changed view name\n    toggle_status_func=toggle_status_samplebook, # add changed view name\n    add_tags_func=add_tags_samplebook, # add changed view name\n    del_tag_func=del_tag_samplebook, # add changed view name\n)\n```\n\nThe copy/pasting isn\'t the most elegant solution but it provides better understanding of the data flow.\n\n### Configure urls.py\n\nDeclare a urls.py\'s `app_name`. The value declared _must match_ the model object\'s `._meta.app_label`.\n\nSo in the `SampleBook` model example above, since model is contained in the `examples` app, can configure the urls.py likeso:\n\n```python\n# examples/urls.py\nfrom .views import SAMPLEBOOK\napp_name = "examples" #  = SampleBook.objects.get(pk=1)._meta.app_label\nurlpatterns = (SAMPLEBOOK.make_patterns())\n```\n\nThe reason for the matching requirement is that `django.urls.reverse()` functions will rely on this convention to call urls from the object instance with a pre-determined property value, e.g.:\n\n```zsh\n>>> book_obj = SampleBook.objects.first()\n>>> book_obj.launch_modal_url\n\'/samplebook/launch_modal/1\'\n>>> quote_obj = SampleQuote.objects.first()\n>>> quote_obj.launch_modal_url\n\'/samplequote/launch_modal/5500a731-7682-4157-9f26-19412e44c560\'\n```\n\n### Override tags/tags.html\n\nIn the `examples` app, we declared two `AbstractBookmarkable` models: `SampleBook` and `SampleQuote`.\n\nThe view `annotated_tags()` effectively determines implemeting classes of `AbstractBookmarkable` and produces the proper annotations.\n\nStill exploring best way to display this annotated list of tags in the template.\n\nFor now, can override the `tags/tags.html` template, replacing `samplebook` and `samplequote`, the _verbose model names_ of the implementing classes, with your selected models.\n\n```jinja\n{% for tag in tags %}\n    ...<!-- Do this for each model that you want to show a count of tags for -->\n    {% include \'./item.html\' with count=tag.samplebook_count slug=tag.samplebook_slug idx=tag.samplebook_id %}\n    {% include \'./item.html\' with count=tag.samplequote_count slug=tag.samplequote_slug idx=tag.samplequote_id %}\n    ...\n{% endfor %}\n```\n\n## Frontend Customization\n\n### Basic design\n\n1. The [base.html](./templates/base.html) uses the htmx/hyperscript example [modal.css](./examples/static/css/modal.css) and a bespoke [starter.css](./examples/static/css/starter.css) declared in an _app-level_ static folder.\n2. The _app-level_ [modal.html](./bookmarks/templates/commons/modal.html) displays custom modals via htmx click.\n3. The _app-level_ [panel.html](./tags/templates/tags/panel.html) provides content displayed within custom modals.\n4. The modal is where backend actions – i.e. toggle bookmark status, add tags, remove tag – become operational.\n\n### Overriding style\n\n1. Modify `base.html` to use [insert _framework_ here].\n2. Declare root-level `templates` directory, add a subdirectory `tags/` with a `panel.html`\n3. Copy and paste the _app-level_ `panel.html` into the directory created in (2.)\n4. Style the root-level `panel.html` based on inserted _framework_.\n\n### UX via htmx & hyperscript\n\n1. Load custom modal based on htmx custom modal [css style](https://htmx.org/examples/modal-custom/):\n\n   ```jinja\n   <!-- adds a custom DOM element before the end of the body, adding a dark underlay to the DOM to highlight the modal\'s contents -->\n   <em hx-trigger="click"\n       hx-get="{{url}}"\n       hx-target="body"\n       hx-swap="beforeend">\n       view\n   </em>\n   ```\n\n2. Post data on checkbox change:\n\n   ```jinja\n   <!-- \'closest section\' assumes a parent container <section> -->\n   <section>\n       <input\n           hx-trigger="change"\n           hx-put="{{url}}"\n           hx-target="closest section"\n           type="checkbox"\n           {% if is_bookmarked %}\n               checked\n           {% endif %}\n       >\n           Bookmark\n       </input>\n   </section>\n   ```\n\n3. Delete from backend and remove frontend element on click\n\n   ```jinja\n   <!-- badge <span>x</span> for easy deletion; when clicked, a DELETE request is sent to the backend. Once deleted, the frontend receives a trigger "tagDeleted" to remove the DOM element targeted by deletion. -->\n   <small id="del_this">\n       Thing to delete\n       <span\n           hx-trigger="click"\n           hx-confirm="Are you sure you want to delete this?"\n           hx-delete="{{url}}"\n           hx-swap="none"\n           _="on tagDeleted transition opacity to 0 then remove #del_this"\n       >x</span>\n   </small>\n   ```\n',
    'author': 'Marcelino G. Veloso III',
    'author_email': 'mars@veloso.one',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/justmars/django-yabl',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
