Skip to content

Commit

Permalink
initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
KommuSoft committed Feb 17, 2025
0 parents commit e9df165
Show file tree
Hide file tree
Showing 19 changed files with 431 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository
custom: https://www.buymeacoffee.com/hapytex
166 changes: 166 additions & 0 deletions .github/workflows/django-user-pinned-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: django-user-pinned CI
on: push
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: psf/black@stable
with:
options: "--check"

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: sudo apt install python3-django
- run: pip install Django
- run: django-admin startproject testproject
- name: checkout code
uses: actions/[email protected]
with:
path: 'testproject_temp'
- run: "mv testproject_temp/* testproject/"
- run: pip install -r requirements.txt
working-directory: 'testproject'
- run: python manage.py test --settings=docs.source.settings
working-directory: 'testproject'

no-makemigrations:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: sudo apt install python3-django
- run: pip install Django
- run: django-admin startproject testproject
- name: checkout code
uses: actions/[email protected]
with:
path: 'testproject_temp'
- run: "mv testproject_temp/* testproject/"
- run: pip install -r requirements.txt
working-directory: 'testproject'
- run: python manage.py makemigrations --dry-run --settings=docs.source.settings
working-directory: 'testproject'
shell: bash

no-makemessages:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
locale: [nl]
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: sudo apt install gettext python3-django
- run: pip install Django
- run: django-admin startproject testproject
- name: checkout code
uses: actions/[email protected]
with:
path: 'testproject_temp'
- run: |
shopt -s dotglob
mv testproject_temp/* testproject/
- run: pip install -r requirements.txt
working-directory: 'testproject'
- run: python manage.py makemessages --locale=${{ matrix.locale }} --settings=docs.source.settings
working-directory: 'testproject'
- run: git diff --ignore-matching-lines='^"POT-Creation-Date:' --exit-code
working-directory: 'testproject'

no-compilemessages:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: sudo apt install gettext python3-django
- run: pip install Django
- run: django-admin startproject testproject
- name: checkout code
uses: actions/[email protected]
with:
path: 'testproject_temp'
- run: |
shopt -s dotglob
mv testproject_temp/* testproject/
- run: pip install -r requirements.txt
working-directory: 'testproject'
- run: python manage.py compilemessages --settings=docs.source.settings
working-directory: 'testproject'
- run: git diff --exit-code
working-directory: 'testproject'

build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- name: checkout code
uses: actions/[email protected]
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: |
pip install setuptools>=38.6.0 twine>=1.11.0 wheel>=0.31.0 setuptools_scm>=6.2
python -m setuptools_scm
python setup.py sdist bdist_wheel
test-publish:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
needs: [black, build, test, no-makemigrations, no-makemessages, no-compilemessages]
steps:
- name: checkout code
uses: actions/[email protected]
- name: Set up Python
uses: actions/setup-python@v4
- run: |
pip install setuptools>=38.6.0 twine>=1.11.0 wheel>=0.31.0 setuptools_scm>=6.2
python -m setuptools_scm
python setup.py sdist bdist_wheel
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_TOKEN }}
repository_url: https://test.pypi.org/legacy/

publish:
runs-on: ubuntu-latest
needs: [test-publish]
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: checkout code
uses: actions/[email protected]
- name: Set up Python
uses: actions/setup-python@v4
- run: |
pip install setuptools>=38.6.0 twine>=1.11.0 wheel>=0.31.0 setuptools_scm>=6.2
python -m setuptools_scm
python setup.py sdist bdist_wheel
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
*.log
*.pot
*.pyc
*.sw[po]
*.sqlite
*.sqlite3

docs/_build
build/
dist/
*.egg-info/
.tox/
.idea/
*.python-version
.coverage

djutils/
manage.py
30 changes: 30 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Copyright Willem Van Onsem (c) 2025

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.

* Neither the name of Willem Van Onsem nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include LICENSE
include README.md
recursive-include django_user_pinned/locale/ **.po **.mo
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Django-user-pinned

[![PyPi version](https://badgen.net/pypi/v/django-user-pinned/)](https://pypi.python.org/pypi/django-user-pinned/)
[![Documentation Status](https://readthedocs.org/projects/django-user-pinned/badge/?version=latest)](http://django-user-pinned.readthedocs.io/?badge=latest)
[![PyPi license](https://badgen.net/pypi/license/django-user-pinned/)](https://pypi.python.org/pypi/django-user-pinned/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

2 changes: 2 additions & 0 deletions django_user_pinned/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__version__ = "0.1"
__author__ = "Willem Van Onsem"
35 changes: 35 additions & 0 deletions django_user_pinned/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.contrib import admin
from django.contrib.auth import get_permission_codename
from django.utils.translation import gettext as _


class PinnableAdminMixin:
def get_queryset(self, request):
# include number of pins and if the current user is a pin
return super().get_queryset(request).with_pinned(request.user).with_pins()

def get_list_display(self, request):
return [*super().get_list_display(request), 'pin_star']

def _get_base_actions(self):
return [*super()._get_base_actions(), *filter(None, map(self.get_action, ('pin', 'unpin')))]

@admin.action(description=_("Pin items"), permissions=['pin'])
def pin(self, request, queryset):
model_name = self.model._meta.model_name
getattr(request.user, f'pinned_{model_name}s').add(*queryset)

def has_pin_permission(self, request):
opts = self.opts
codename = get_permission_codename('pin', opts)
return request.user.has_perm(f'{opts.app_label}.{codename}')

@admin.action(description=_("Unpin items"), permissions=['unpin'])
def unpin(self, request, queryset):
model_name = self.model._meta.model_name
getattr(request.user, f'pinned_{model_name}s').remove(*queryset)

def has_unpin_permission(self, request):
opts = self.opts
codename = get_permission_codename('unpin', opts)
return request.user.has_perm(f'{opts.app_label}.{codename}')
11 changes: 11 additions & 0 deletions django_user_pinned/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig


class SingleSessionConfig(AppConfig):
"""
The app config for the single_session app.
"""

name = "django_user_pinned"
verbose_name = "django user pinned"
default_auto_field = "django.db.models.BigAutoField"
35 changes: 35 additions & 0 deletions django_user_pinned/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2025 Willem Van Onsem
# This file is distributed under the same license as the PACKAGE package.
# Willem Van Onsem <[email protected]>, 2025.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-16 15:05+0100\n"
"PO-Revision-Date: 2025-02-16 15:05+0100\n"
"Last-Translator: Willem Van Onsem <[email protected]>\n"
"Language-Team: NL <[email protected]>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: django_user_pinned/admin.py:21
msgid "Pin items"
msgstr "Markeer als favoriet"

#: django_user_pinned/admin.py:31
msgid "Unpin items"
msgstr "Verwijder als favoriet"

#: django_user_pinned/models.py:68
msgid "a user can add an object to their selection"
msgstr "Een gebruiker kan objecten aan zijn/haar selectie toevoegen"

#: django_user_pinned/models.py:69
msgid "a user can remove an object from their selection"
msgstr "Een gebruiker kan objecten van zijn/haar selectie verwijderen"
Empty file.
70 changes: 70 additions & 0 deletions django_user_pinned/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from django.db import models
from django.conf import settings
from django.db.models.aggregates import Count
from django.db.models.expressions import Exists, OuterRef
from django.db.models.query_utils import Q
from django.contrib import admin
from django.utils.translation import gettext as _


STAR_CHARS = '☆★'


class PinnableQuerySet(models.QuerySet):
def with_pinned(self, user, *users):
model = self.model
rel = model.pinned_by
junction = rel.through
rel_name = rel.rel.model._meta.model_name
if users:
rel_name = f'{rel_name}__in'
user = (user, *users)
return self.annotate(
is_pinned=Exists(junction.objects.filter(
Q((self.model._meta.model_name, OuterRef('pk')),
Q((rel_name, user))
)))
)

def with_pinned_sorted(self, user, *users):
return self.get_pinned(user, *users).order_by('-is_pinned')

def with_pins(self):
return self.annotate(_pins=Count('pinned_by', distinct=True))

def with_pins_sorted(self):
return self.with_pins().order_by('-_pins')


class PinnableModel(models.Model):
pinned_by = models.ManyToManyField(settings.AUTH_USER_MODEL, symmetrical=False, related_name='pinned_%(class)ss', blank=True, editable=False)
objects = PinnableQuerySet.as_manager()

@property
def pins(self):
pins = getattr(self, '_pins', None)
if pins is None:
self._pins = pins = self.pinned_by.count()
return pins

@property
def pin_star_char(self):
return STAR_CHARS[bool(getattr(self, 'is_pinned', False))]

@property
@admin.display(description='pins', ordering='_pins')
def pin_star(self):
return f'{self.pins} {self.pin_star_char}'

def pin(self, user, *users):
self.pinned_by.add(user, *users)

def unpin(self, user, *users):
self.pinned_by.remove(user, *users)

class Meta:
abstract = True
permissions = [
('pin', _('a user can add an object to their selection')),
('unpin', _('a user can remove an object from their selection')),
]
Empty file added django_user_pinned/signals.py
Empty file.
Empty file added django_user_pinned/tests.py
Empty file.
Empty file added django_user_pinned/views.py
Empty file.
Loading

0 comments on commit e9df165

Please sign in to comment.