From 38f10579aef4ec1bf69fc76eb51f7924dd4bd7d6 Mon Sep 17 00:00:00 2001 From: Hugo Rodger-Brown Date: Thu, 2 Nov 2023 14:11:25 +0000 Subject: [PATCH] Update python / django support (#26) * Update python / django support * Remove default app_config * Apply linting * Add request context processor to TEMPLATES * Add freezegun to test deps --- .flake8 | 39 ----------- .github/workflows/tox.yml | 67 ++++++++++++------- .isort.cfg | 8 --- .pre-commit-config.yaml | 33 ++------- .ruff.toml | 66 ++++++++++++++++++ CHANGELOG | 7 +- LICENSE | 2 +- README.md | 26 ++++--- pyproject.toml | 21 +++--- tests/settings.py | 1 + tests/test_middleware.py | 20 +++--- tests/test_models.py | 20 +++--- tests/utils.py | 2 +- tox.ini | 47 +++++++++---- user_visit/__init__.py | 1 - user_visit/admin.py | 1 - user_visit/apps.py | 2 +- user_visit/migrations/0001_initial.py | 1 - user_visit/migrations/0002_add_created_at.py | 1 - .../migrations/0003_uservisit_context.py | 1 - user_visit/models.py | 2 +- 21 files changed, 198 insertions(+), 170 deletions(-) delete mode 100644 .flake8 delete mode 100644 .isort.cfg create mode 100644 .ruff.toml diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1859042..0000000 --- a/.flake8 +++ /dev/null @@ -1,39 +0,0 @@ -[flake8] -max-line-length = 88 -max-complexity = 8 -# http://flake8.pycqa.org/en/2.5.5/warnings.html#warning-error-codes -ignore = - # pydocstyle - docstring conventions (PEP257) - D100 # Missing docstring in public module - D101 # Missing docstring in public class - D102 # Missing docstring in public method - D103 # Missing docstring in public function - D104 # Missing docstring in public package - D105 # Missing docstring in magic method - D106 # Missing docstring in public nested class - D107 # Missing docstring in __init__ - D412 # No blank lines allowed between a section header and its content - # pycodestyle - style checker (PEP8) - W503 # line break before binary operator - # the following are ignored in CI using --extend-ignore option: - ; D205 # [pydocstyle] 1 blank line required between summary line and description - ; D400 # [pydocstyle] First line should end with a period - ; D401 # [pydocstyle] First line should be in imperative mood - ; S308 # [bandit] Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed. - ; S703 # [bandit] Potential XSS on mark_safe function. - -per-file-ignores = - ; D205 - 1 blank line required between summary line and description - ; D400 - First line should end with a period - ; D401 - First line should be in imperative mood - ; S101 - use of assert - ; S105 - possible hard-coded password - ; S106 - hard-coded password - ; E501 - line-length - ; E731 - assigning a lambda to a variable - *tests/*:D205,D400,D401,S101,S105,S106,E501,E731 - */migrations/*:E501 - ; F403 - unable to detect undefined names - ; F405 - may be undefined, or defined from star imports - */settings.py:F403,F405 - */settings/*:F403,F405 diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 6aa3b70..fde0a37 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -14,18 +14,41 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toxenv: [fmt,lint,mypy] + toxenv: [fmt, lint, mypy] env: TOXENV: ${{ matrix.toxenv }} steps: - name: Check out the repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v1 + - name: Set up Python (3.11) + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" + + - name: Install and run tox + run: | + pip install tox + tox + + checks: + name: Run Django tests + runs-on: ubuntu-latest + strategy: + matrix: + toxenv: ["django-checks"] + env: + TOXENV: ${{ matrix.toxenv }} + + steps: + - name: Check out the repository + uses: actions/checkout@v3 + + - name: Set up Python (3.11) + uses: actions/setup-python@v4 + with: + python-version: "3.11" - name: Install and run tox run: | @@ -37,35 +60,27 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.10"] - django: [32,40,main] + python: ["3.8", "3.9", "3.10", "3.11"] + django: ["32", "40", "41", "42", "50", "main"] + exclude: + - python: "3.8" + django: "50" + - python: "3.8" + django: "main" + - python: "3.9" + django: "50" + - python: "3.9" + django: "main" env: TOXENV: py${{ matrix.python }}-django${{ matrix.django }} - # services: - # postgres: - # image: postgres:12 - # env: - # POSTGRES_USER: postgres - # POSTGRES_PASSWORD: postgres - # POSTGRES_DB: onfido - # # Set health checks to wait until postgres has started - # options: >- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # ports: - # # Maps tcp port 5432 on service container to the host - # - 5432:5432 - steps: - name: Check out the repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 629cb68..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[settings] -default_section=THIRDPARTY -indent=' ' -sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -multi_line_output=3 -line_length=88 -include_trailing_comma=True -use_parentheses=True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7171ef..f55f382 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,40 +1,22 @@ repos: - # python import sorting - will amend files - - repo: https://github.com/pre-commit/mirrors-isort - rev: v5.10.1 - hooks: - - id: isort - # python code formatting - will amend files - repo: https://github.com/ambv/black - rev: 22.10.0 + rev: 23.10.1 hooks: - id: black - - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 - hooks: - - id: pyupgrade - - # Flake8 includes pyflakes, pycodestyle, mccabe, pydocstyle, bandit - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: "v0.1.3" hooks: - - id: flake8 - additional_dependencies: - - flake8-bandit - - flake8-blind-except - - flake8-docstrings - - flake8-logging-format - - flake8-print + - id: ruff + args: [--fix, --exit-non-zero-on-fix] # python static type checking - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v1.6.1 hooks: - id: mypy - additional_dependencies: - - types-geoip2 args: - --disallow-untyped-defs - --disallow-incomplete-defs @@ -42,4 +24,3 @@ repos: - --no-implicit-optional - --ignore-missing-imports - --follow-imports=silent - exclude: ^tests diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..ccc1947 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,66 @@ +line-length = 88 +ignore = [ + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in __init__ + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be "This" + "D405", # Section name should be properly capitalized + "D406", # Section name should end with a newline + "D407", # Missing dashed underline after section + "D410", # Missing blank line after section + "D411", # Missing blank line before section + "D412", # No blank lines allowed between a section header and its content + "D416", # Section name should end with a colon + "D417", + "D417", # Missing argument description in the docstring +] +select = [ + "A", # flake8 builtins + "C9", # mcabe + "D", # pydocstyle + "E", # pycodestyle (errors) + "F", # Pyflakes + "I", # isort + "S", # flake8-bandit + "T2", # flake8-print + "W", # pycodestype (warnings) +] + +[isort] +combine-as-imports = true + +[mccabe] +max-complexity = 8 + +[per-file-ignores] +"*tests/*" = [ + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D401", # First line should be in imperative mood + "D415", # First line should end with a period, question mark, or exclamation point + "E501", # Line too long + "E731", # Do not assign a lambda expression, use a def + "S101", # Use of assert detected + "S105", # Possible hardcoded password + "S106", # Possible hardcoded password + "S113", # Probable use of requests call with timeout set to {value} +] +"*/migrations/*" = [ + "E501", # Line too long +] +"*/settings.py" = [ + "F403", # from {name} import * used; unable to detect undefined names + "F405", # {name} may be undefined, or defined from star imports: +] +"*/settings/*" = [ + "F403", # from {name} import * used; unable to detect undefined names + "F405", # {name} may be undefined, or defined from star imports: +] diff --git a/CHANGELOG b/CHANGELOG index 6c77b46..a89adca 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. -## 1.1 [Unreleased] +## 2.0 [unreleased] + +* Add support for Django 4.1,4.2,5.0 and Python 3.11, 3.12 +* Replace flake8, isort with ruff. + +## 1.1 * Added support for controlling logging duplicates (#20) diff --git a/LICENSE b/LICENSE index bf71245..67a0b8c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 yunojuno +Copyright (c) 2023 yunojuno Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5602ea4..6e8f9f3 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,24 @@ Django app for recording daily user visits #### Compatibility -This library uses the `__future__.annotations` import for postponed evaluation of annotations. -As a result it supports Python 3.7 and above only. - -It supports Django 2.2 and above. +This package supports Python 3.8 and above and Django 3.2 and above. --- -This app consists of middleware to record user visits, and a single `UserVisit` model to capture -that data. +This app consists of middleware to record user visits, and a single +`UserVisit` model to capture that data. -The principal behind this is _not_ to record every single request made by a user. It is to record -each daily visit to a site. +The principal behind this is _not_ to record every single request made +by a user. It is to record each daily visit to a site. -The one additional factor is that it will record a single daily visit per session / device / ip -combination. This means that if a user visits a site multiple times from the same location / same -device, without logging out, then they will be recorded once. If the same user logs in from a -different device, IP address, then they will be recorded again. +The one additional factor is that it will record a single daily visit +per session / device / ip combination. This means that if a user visits +a site multiple times from the same location / same device, without +logging out, then they will be recorded once. If the same user logs in +from a different device, IP address, then they will be recorded again. -The goal is to record unique daily visits per user 'context' ( where context is the location / -device combo). +The goal is to record unique daily visits per user 'context' ( where +context is the location / device combo). Admin list view: diff --git a/pyproject.toml b/pyproject.toml index 7a7a272..1fb62cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-user-visit" -version = "1.1" +version = "2.0.dev0" description = "Django app used to track user visits." license = "MIT" authors = ["YunoJuno "] @@ -12,9 +12,11 @@ documentation = "https://github.com/yunojuno/django-user-visit" classifiers = [ "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", @@ -22,31 +24,26 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] packages = [{ include = "user_visit" }] [tool.poetry.dependencies] -python = "^3.7" -django = "^3.1 || ^4.0" +python = "^3.8" +django = "^3.2 || ^4.0 || ^5.0" user-agents = "^2.1" [tool.poetry.dev-dependencies] -bandit = "1.7.2" black = {version = "*", allow-prereleases = true} coverage = "*" -flake8 = "*" -flake8-bandit = "*" -flake8-blind-except = "*" -flake8-docstrings = "*" -flake8-logging-format = "*" -flake8-print = "*" freezegun = "*" -isort = "*" mypy = "*" pre-commit = "*" pytest = "*" pytest-cov = "*" pytest-django = "*" +ruff = "*" tox = "*" [build-system] diff --git a/tests/settings.py b/tests/settings.py index 835b503..64e9b31 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -42,6 +42,7 @@ "context_processors": [ "django.contrib.messages.context_processors.messages", "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", ] }, } diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 9d0dac9..8a92f2d 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -14,7 +14,7 @@ @pytest.mark.django_db -def test_save_user_visit(): +def test_save_user_visit() -> None: """Test standalone save method handles db.IntegrityError.""" user = User.objects.create(username="Yoda") timestamp = timezone.now() @@ -31,7 +31,7 @@ def test_save_user_visit(): @pytest.mark.django_db @mock.patch("user_visit.middleware.logger") -def test_save_user_visit__duplicate(mock_logger): +def test_save_user_visit__duplicate(mock_logger: mock.Mock) -> None: """Test standalone save method handles db.IntegrityError.""" user = User.objects.create(username="Yoda") timestamp = timezone.now() @@ -51,7 +51,7 @@ def test_save_user_visit__duplicate(mock_logger): @pytest.mark.django_db @mock.patch("user_visit.middleware.logger") @mock.patch("user_visit.middleware.DUPLICATE_LOG_LEVEL", "debug") -def test_save_user_visit__duplicate__log_levels(mock_logger): +def test_save_user_visit__duplicate__log_levels(mock_logger: mock.Mock) -> None: """Test standalone save method handles db.IntegrityError.""" user = User.objects.create(username="Yoda") timestamp = timezone.now() @@ -72,24 +72,24 @@ def test_save_user_visit__duplicate__log_levels(mock_logger): class TestUserVisitMiddleware: """RequestTokenMiddleware tests.""" - def get_middleware(self): + def get_middleware(self) -> UserVisitMiddleware: return UserVisitMiddleware(get_response=lambda r: HttpResponse()) - def test_middleware__anon(self): + def test_middleware__anon(self) -> None: """Check that anonymous users are ignored.""" client = Client() with mock.patch.object(UserVisitManager, "build") as build: client.get("/") assert build.call_count == 0 - def test_middleware__auth(self): + def test_middleware__auth(self) -> None: """Check that authenticated users are recorded.""" client = Client() client.force_login(User.objects.create_user("Fred")) client.get("/") assert UserVisit.objects.count() == 1 - def test_middleware__same_day(self): + def test_middleware__same_day(self) -> None: """Check that same user, same day, gets only one visit recorded.""" client = Client() client.force_login(User.objects.create_user("Fred")) @@ -97,7 +97,7 @@ def test_middleware__same_day(self): client.get("/") assert UserVisit.objects.count() == 1 - def test_middleware__new_day(self): + def test_middleware__new_day(self) -> None: """Check that same user, new day, gets new visit.""" user = User.objects.create_user("Fred") client = Client() @@ -110,7 +110,7 @@ def test_middleware__new_day(self): client.get("/") assert UserVisit.objects.count() == 2 - def test_middleware__db_integrity_error(self): + def test_middleware__db_integrity_error(self) -> None: """Check that a failing save doesn't kill middleware.""" user = User.objects.create_user("Fred") client = Client() @@ -119,7 +119,7 @@ def test_middleware__db_integrity_error(self): client.get("/") @mock.patch("user_visit.middleware.RECORDING_DISABLED", True) - def test_middleware__disabled(self): + def test_middleware__disabled(self) -> None: """Test update_cache and check_cache functions.""" with pytest.raises(MiddlewareNotUsed): UserVisitMiddleware(get_response=lambda r: HttpResponse()) diff --git a/tests/test_models.py b/tests/test_models.py index 759f12b..30806be 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -25,21 +25,21 @@ class TestUserVisitFunctions: ("", "192.168.0.1", "192.168.0.1"), ), ) - def test_remote_addr(self, xff, remote, output): + def test_remote_addr(self, xff: str, remote: str, output: str) -> None: request = mock_request() request.headers["X-Forwarded-For"] = xff request.META["REMOTE_ADDR"] = remote assert parse_remote_addr(request) == output @pytest.mark.parametrize("ua_string", ("", "Chrome")) - def test_ua_string(self, ua_string): + def test_ua_string(self, ua_string: str) -> None: request = mock_request() request.headers["User-Agent"] = ua_string assert parse_ua_string(request) == ua_string class TestUserVisitManager: - def test_build(self): + def test_build(self) -> None: request = mock_request() timestamp = timezone.now() uv = UserVisit.objects.build(request, timestamp) @@ -53,7 +53,7 @@ def test_build(self): assert uv.uuid is not None assert uv.pk is None - def test_build__REQUEST_CONTEXT_EXTRACTOR(self): + def test_build__REQUEST_CONTEXT_EXTRACTOR(self) -> None: request = mock_request() timestamp = timezone.now() extractor = lambda r: {"foo": "bar"} @@ -63,15 +63,14 @@ def test_build__REQUEST_CONTEXT_EXTRACTOR(self): class TestUserVisit: - UA_STRING = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" - def test_user_agent(self): + def test_user_agent(self) -> None: uv = UserVisit(ua_string=TestUserVisit.UA_STRING) assert str(uv.user_agent) == "PC / Mac OS X 10.15.5 / Chrome 83.0.4103" @pytest.mark.django_db - def test_save(self): + def test_save(self) -> None: request = mock_request() request.user.save() timestamp = timezone.now() @@ -79,11 +78,10 @@ def test_save(self): uv.hash = None uv.context = {"foo": "bar"} uv.save() - assert uv.hash is not None assert uv.hash == uv.md5().hexdigest() @pytest.mark.django_db - def test_unique(self): + def test_unique(self) -> None: """Check that visits on the same day but at different times, are rejected.""" user = User.objects.create(username="Bob") timestamp1 = timezone.now() @@ -106,7 +104,7 @@ def test_unique(self): uv2.save() @pytest.mark.django_db - def test_get_latest_by(self): + def test_get_latest_by(self) -> None: """Check that latest() is ordered by timestamp, not id.""" user = User.objects.create(username="Bob") timestamp1 = timezone.now() @@ -128,7 +126,7 @@ def test_get_latest_by(self): assert uv1.timestamp > uv2.timestamp assert user.user_visits.latest() == uv1 - def test_md5(self): + def test_md5(self) -> None: """Check that MD5 changes when properties change.""" uv = UserVisit( user=User(), diff --git a/tests/utils.py b/tests/utils.py index 52239b4..31eb2e6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ from django.http import HttpRequest -def mock_request(is_authenticated=True): +def mock_request(is_authenticated: bool = True) -> mock.Mock: return mock.Mock( spec=HttpRequest, user=User() if is_authenticated else AnonymousUser(), diff --git a/tox.ini b/tox.ini index 8ec3989..ca24cbe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,42 +1,61 @@ [tox] -isolated_build = true -envlist = fmt, lint, mypy, py{3.7,3.8,3.9,3.10}-django{31,32,40,main} +isolated_build = True +envlist = + fmt, lint, mypy, + django-checks, + ; https://docs.djangoproject.com/en/5.0/releases/ + ; | 3.2 | 4.2 | 5.0 | main + ; --- | --- | --- | --- | --- + ; 3.8 | x | x | - | - + ; 3.9 | x | x | - | - + ; 3.10 | x | x | x | x + ; 3.11 | - | x | x | x + ; 3.12 | - | - | x | x + py38-django{32,40,41,42} + py39-django{32,40,41,42} + py310-django{32,40,41,42,50,main} + py311-django{32,40,41,42,50,main} + py312-django{32,40,41,42,50,main} [testenv] -whitelist_externals = poetry deps = coverage freezegun pytest pytest-cov pytest-django - django{22,30}: psycopg2-binary - django31: Django>=3.1,<3.2 + django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 + django50: https://github.com/django/django/archive/stable/5.0.x.tar.gz djangomain: https://github.com/django/django/archive/main.tar.gz commands = - pytest --cov=user_visit tests/ + pytest --cov=user_visit --verbose tests/ + +[testenv:django-checks] +description = Django system checks and missing migrations +deps = Django +commands = + python manage.py check --fail-level WARNING + python manage.py makemigrations --dry-run --check --verbosity 3 [testenv:fmt] -description = Python source code formatting (isort, black) +description = Python source code formatting (black) deps = - isort black commands = - isort --check-only user_visit black --check user_visit [testenv:lint] -description = Python source code linting (flake8, bandit, pydocstyle) +description = Python source code linting (ruff) deps = - flake8 - flake8-bandit - flake8-docstrings + ruff commands = - flake8 user_visit + ruff user_visit [testenv:mypy] description = Python source code type hints (mypy) diff --git a/user_visit/__init__.py b/user_visit/__init__.py index d37a76d..e69de29 100644 --- a/user_visit/__init__.py +++ b/user_visit/__init__.py @@ -1 +0,0 @@ -default_app_config = "user_visit.apps.UserVisitAppConfig" diff --git a/user_visit/admin.py b/user_visit/admin.py index d59b1e0..4e5960d 100644 --- a/user_visit/admin.py +++ b/user_visit/admin.py @@ -4,7 +4,6 @@ class UserVisitAdmin(admin.ModelAdmin): - list_display = ("timestamp", "user", "session_key", "remote_addr", "user_agent") list_filter = ("timestamp",) search_fields = ( diff --git a/user_visit/apps.py b/user_visit/apps.py index 4f77959..36ae94a 100644 --- a/user_visit/apps.py +++ b/user_visit/apps.py @@ -2,6 +2,6 @@ class UserVisitAppConfig(AppConfig): - name = "user_visit" verbose_name = "User visit log" + default_auto_field = "django.db.models.AutoField" diff --git a/user_visit/migrations/0001_initial.py b/user_visit/migrations/0001_initial.py index 36098da..0e2975e 100644 --- a/user_visit/migrations/0001_initial.py +++ b/user_visit/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/user_visit/migrations/0002_add_created_at.py b/user_visit/migrations/0002_add_created_at.py index 1364104..ed97844 100644 --- a/user_visit/migrations/0002_add_created_at.py +++ b/user_visit/migrations/0002_add_created_at.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("user_visit", "0001_initial"), ] diff --git a/user_visit/migrations/0003_uservisit_context.py b/user_visit/migrations/0003_uservisit_context.py index ae3db1a..89f0c9e 100644 --- a/user_visit/migrations/0003_uservisit_context.py +++ b/user_visit/migrations/0003_uservisit_context.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("user_visit", "0002_add_created_at"), ] diff --git a/user_visit/models.py b/user_visit/models.py index eea6354..5358840 100644 --- a/user_visit/models.py +++ b/user_visit/models.py @@ -83,7 +83,7 @@ class UserVisit(models.Model): blank=True, ) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - hash = models.CharField( + hash = models.CharField( # noqa: A003 max_length=32, help_text=_lazy("MD5 hash generated from request properties"), unique=True,