diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 7ca2cc0..ef4f2c3 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -17,7 +17,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install pylint
+ pip install pylint django
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Run Pylint
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..c527e0b
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,654 @@
+[MAIN]
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Clear in-memory caches upon conclusion of linting. Useful if running pylint
+# in a server-like mode.
+clear-cache-post-run=no
+
+# Load and enable all available extensions. Use --list-extensions to see a list
+# all available extensions.
+#enable-all-extensions=
+
+# In error mode, messages with a category besides ERROR or FATAL are
+# suppressed, and no reports are done by default. Error mode is compatible with
+# disabling specific errors.
+#errors-only=
+
+# Always return a 0 (non-error) status code, even if lint errors are found.
+# This is primarily useful in continuous integration scripts.
+#exit-zero=
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-allow-list=
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
+# for backward compatibility.)
+extension-pkg-whitelist=
+
+# Return non-zero exit code if any of these messages/categories are detected,
+# even if score is above --fail-under value. Syntax same as enable. Messages
+# specified are enabled, while categories only check already-enabled messages.
+fail-on=
+
+# Specify a score threshold under which the program will exit with error.
+fail-under=10
+
+# Interpret the stdin as a python script, whose filename needs to be passed as
+# the module_or_package argument.
+#from-stdin=
+
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=CVS, migrations, tests
+
+# Add files or directories matching the regular expressions patterns to the
+# ignore-list. The regex matches against paths and can be in Posix or Windows
+# format. Because '\\' represents the directory delimiter on Windows systems,
+# it can't be used as an escape character.
+ignore-paths=
+
+# Files or directories matching the regular expression patterns are skipped.
+# The regex matches against base names, not paths. The default value ignores
+# Emacs file locks
+ignore-patterns=^\.#
+
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well as
+# Unix pattern matching.
+ignored-modules=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use, and will cap the count on Windows to
+# avoid hangs.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Resolve imports to .pyi stubs if available. May reduce no-member messages and
+# increase not-an-iterable messages.
+prefer-stubs=no
+
+# Minimum Python version to use for version dependent checks. Will default to
+# the version used to run pylint.
+py-version=3.12
+
+# Discover python modules and packages in the file system subtree.
+recursive=no
+
+# Add paths to the list of the source roots. Supports globbing patterns. The
+# source root is an absolute path or a path relative to the current working
+# directory used to determine a package namespace for modules located under the
+# source root.
+source-roots=
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# In verbose mode, extra non-checker-related info will be displayed.
+#verbose=
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style. If left empty, argument names will be checked with the set
+# naming style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style. If left empty, attribute names will be checked with the set naming
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style. If left empty, class attribute names will be checked
+# with the set naming style.
+#class-attribute-rgx=
+
+# Naming style matching correct class constant names.
+class-const-naming-style=UPPER_CASE
+
+# Regular expression matching correct class constant names. Overrides class-
+# const-naming-style. If left empty, class constant names will be checked with
+# the set naming style.
+#class-const-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style. If left empty, class names will be checked with the set naming style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style. If left empty, constant names will be checked with the set naming
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style. If left empty, function names will be checked with the set
+# naming style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style. If left empty, inline iteration names will be checked
+# with the set naming style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style. If left empty, method names will be checked with the set naming style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style. If left empty, module names will be checked with the set naming style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Regular expression matching correct type alias names. If left empty, type
+# alias names will be checked with the set naming style.
+#typealias-rgx=
+
+# Regular expression matching correct type variable names. If left empty, type
+# variable names will be checked with the set naming style.
+#typevar-rgx=
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style. If left empty, variable names will be checked with the set
+# naming style.
+#variable-rgx=
+
+
+[CLASSES]
+
+# Warn about protected attribute access inside special methods
+check-protected-access-in-special-methods=no
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ asyncSetUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# List of regular expressions of class ancestor names to ignore when counting
+# public methods (see R0903)
+exclude-too-few-public-methods=
+
+# List of qualified class names to ignore when counting class parents (see
+# R0901)
+ignored-parents=
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of positional arguments for function / method.
+max-positional-arguments=5
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when caught.
+overgeneral-exceptions=builtins.BaseException,builtins.Exception
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow explicit reexports by alias from a package __init__.
+allow-reexport-from-package=no
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=
+
+# Output a graph (.gv or any supported image format) of external dependencies
+# to the given file (report RP0402 must not be disabled).
+ext-import-graph=
+
+# Output a graph (.gv or any supported image format) of all (i.e. internal and
+# external) dependencies to the given file (report RP0402 must not be
+# disabled).
+import-graph=
+
+# Output a graph (.gv or any supported image format) of internal dependencies
+# to the given file (report RP0402 must not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
+# UNDEFINED.
+confidence=HIGH,
+ CONTROL_FLOW,
+ INFERENCE,
+ INFERENCE_FAILURE,
+ UNDEFINED
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then re-enable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ use-implicit-booleaness-not-comparison-to-string,
+ use-implicit-booleaness-not-comparison-to-zero,
+ no-member,
+ import-outside-toplevel,
+ unused-argument,
+ too-few-public-methods,
+ too-many-ancestors,
+ unsupported-membership-test,
+ raise-missing-from,
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=
+
+
+[METHOD_ARGS]
+
+# List of qualified names (i.e., library.method) which require a timeout
+# parameter e.g. 'requests.api.get,requests.api.post'
+timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+
+# Regular expression of note tags to take in consideration.
+notes-rgx=
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit,argparse.parse_error
+
+# Let 'consider-using-join' be raised when the separator to join on would be
+# non-empty (resulting in expected fixes of the type: ``"- " + " -
+# ".join(items)``)
+suggest-join-with-non-empty-separator=yes
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'fatal', 'error', 'warning', 'refactor',
+# 'convention', and 'info' which contain the number of messages in each
+# category, as well as 'statement' which is the total number of statements
+# analyzed. This score is used by the global evaluation report (RP0004).
+evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+msg-template=
+
+# Set the output format. Available formats are: text, parseable, colorized,
+# json2 (improved json format), json (old json format) and msvs (visual
+# studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+#output-format=
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[SIMILARITIES]
+
+# Comments are removed from the similarity computation
+ignore-comments=yes
+
+# Docstrings are removed from the similarity computation
+ignore-docstrings=yes
+
+# Imports are removed from the similarity computation
+ignore-imports=yes
+
+# Signatures are removed from the similarity computation
+ignore-signatures=yes
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. No available dictionaries : You need to install
+# both the python package and the system dependency for enchant to work.
+spelling-dict=
+
+# List of comma separated words that should be considered directives if they
+# appear at the beginning of a comment and should not be checked.
+spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of symbolic message names to ignore for Mixin members.
+ignored-checks-for-mixins=no-member,
+ not-async-context-manager,
+ not-context-manager,
+ attribute-defined-outside-init
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# Regex pattern to define which classes are considered mixins.
+mixin-class-rgx=.*[Mm]ixin
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of names allowed to shadow builtins
+allowed-redefined-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
diff --git a/core/__init__.py b/core/__init__.py
index 370372a..51982bb 100644
--- a/core/__init__.py
+++ b/core/__init__.py
@@ -1,3 +1,19 @@
+"""
+core/__init__.py
+================
+
+This module serves as the entry point for the `core` package of the project.
+The `core` package is typicallydesigned to contain fundamental components,
+utilities, or functionality that is used throughout the project.
+
+Module Overview:
+----------------
+The `core` package acts as a central hub for core utilities, shared components,
+or foundational services required by other parts of the application.
+Specific functionality or implementations for the project may be further
+broken into submodules or packages within `core`.
+"""
+
from .celery import app as celery_app
__all__ = ["celery_app"]
diff --git a/core/celery.py b/core/celery.py
index 5aec621..a30df0f 100644
--- a/core/celery.py
+++ b/core/celery.py
@@ -1,7 +1,14 @@
+"""
+celery.py
+=========
+
+This module configures and initializes the Celery application
+used for handling asynchronous tasks in the project.
+"""
import os
-from celery import Celery
from django.conf import settings
+from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
diff --git a/core/settings.py b/core/settings.py
index 43f595e..9def112 100644
--- a/core/settings.py
+++ b/core/settings.py
@@ -28,7 +28,7 @@
SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True if os.environ.get("DEBUG") == "True" else False
+DEBUG = os.environ.get("DEBUG") == "True"
ALLOWED_HOSTS = (
[
diff --git a/shared/models.py b/shared/models.py
index b2c56c8..8bfd3b9 100644
--- a/shared/models.py
+++ b/shared/models.py
@@ -1,3 +1,6 @@
+"""
+A `shared.models` model that contains all shared models
+"""
import uuid
from django.db import models
@@ -10,7 +13,8 @@ class AbstractModel(models.Model):
Fields:
- id (UUIDField): A unique identifier for each instance, automatically generated.
- created_at (DateTimeField): The timestamp when the instance was created, set automatically.
- - updated_at (DateTimeField): The timestamp when the instance was last updated, updated automatically.
+ - updated_at (DateTimeField): The timestamp when the instance was last updated,
+ updated automatically.
This class is intended to be used as a base class for other models, providing a
consistent structure for commonly used fields. As an abstract model, it will not
@@ -24,5 +28,6 @@ class AbstractModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
- class Meta:
+ class Meta: # pylint: disable=too-few-public-methods
+ """A metaclass to set this model to be abstract"""
abstract = True
diff --git a/tracker/__init__.py b/tracker/__init__.py
index f84f2c4..c25956e 100644
--- a/tracker/__init__.py
+++ b/tracker/__init__.py
@@ -1,2 +1,6 @@
+"""
+A `tracker.__init__` module
+"""
+
from tracker.utils import get_issues_without_pull_requests
from tracker.values import ISSUES_URL, PULLS_URL
diff --git a/tracker/admin.py b/tracker/admin.py
index ea3c7a7..0eac5fb 100644
--- a/tracker/admin.py
+++ b/tracker/admin.py
@@ -1,3 +1,7 @@
+"""
+A `tracker.admin` module that manages tracker app on Django admin site
+"""
+
import asyncio
from django.contrib import admin
@@ -14,9 +18,9 @@
SolarSchedule,
)
-from .choices import Roles
from .bases import PredefinedUserAdminBase
-from .models import Contributor, Repository, Support, CustomUser
+from .choices import Roles
+from .models import Contributor, CustomUser, Repository, Support
from .telegram.bot import create_tg_link
admin.site.unregister(Group)
@@ -96,11 +100,12 @@ def has_module_permission(self, request) -> bool:
return False
- def get_form(self, request, obj=None, **kwargs) -> BaseModelForm:
+ def get_form(self, request, obj=None, change=False, **kwargs) -> BaseModelForm:
"""
Customizes the model form to set the user field to the current user.
:param request: HttpRequest
:param obj: AbstractModel
+ :param change: bool
:param kwargs: dict
:return: BaseModelForm
"""
@@ -108,7 +113,9 @@ def get_form(self, request, obj=None, **kwargs) -> BaseModelForm:
form.base_fields["role"].initial = Roles.CONTRIBUTOR
form.base_fields["role"].disabled = True
- form.base_fields["user"].queryset = CustomUser.objects.filter(role=Roles.CONTRIBUTOR)
+ form.base_fields["user"].queryset = CustomUser.objects.filter(
+ role=Roles.CONTRIBUTOR
+ )
return form
@@ -120,7 +127,8 @@ class SupportAdmin(PredefinedUserAdminBase):
Methods:
has_module_permissions: Displays the model only for project leads.
- get_form: Customizes the model form to set the repository field to the current user's repositories.
+ get_form: Customizes the model form to set the repository
+ field to the current user's repositories.
"""
def has_module_permission(self, request) -> bool:
@@ -136,9 +144,19 @@ def has_module_permission(self, request) -> bool:
return False
- def get_form(self, request, obj=None, **kwargs) -> BaseModelForm:
+ def get_form(self, request, obj=None, change=False, **kwargs) -> BaseModelForm:
+ """
+ Returns a support admin form
+ :param request: HttpRequest
+ :param obj: object
+ :param change: bool
+ :param kwargs: dict
+ :return: BaseModelForm
+ """
form = super().get_form(request, obj, **kwargs)
- form.base_fields["repository"].queryset = Repository.objects.filter(user=request.user)
+ form.base_fields["repository"].queryset = Repository.objects.filter(
+ user=request.user
+ )
return form
diff --git a/tracker/apps.py b/tracker/apps.py
index 37951d5..b490c9b 100644
--- a/tracker/apps.py
+++ b/tracker/apps.py
@@ -1,6 +1,14 @@
+"""
+A `tracker.apps` module that creates an app isntance
+"""
+
from django.apps import AppConfig
class TrackerConfig(AppConfig):
+ """
+ A class that creates an app isntance
+ """
+
default_auto_field = "django.db.models.BigAutoField"
name = "tracker"
diff --git a/tracker/bases.py b/tracker/bases.py
index de0ed93..d0a4e41 100644
--- a/tracker/bases.py
+++ b/tracker/bases.py
@@ -1,3 +1,6 @@
+"""
+A `tracker.bases` module that contains all base classes
+"""
from django.contrib import admin
from django.forms import BaseModelForm
@@ -10,11 +13,12 @@ class PredefinedUserAdminBase(admin.ModelAdmin):
get_form: Customizes the model form to set the user field to the current user.
"""
- def get_form(self, request, obj=None, **kwargs) -> BaseModelForm:
+ def get_form(self, request, obj=None, change=False, **kwargs) -> BaseModelForm:
"""
A custom method to set the user field to the current user.
:param request: HttpRequest
:param obj: Repository
+ :param change: bool
:param kwargs: dict
:return: BaseModelForm
"""
diff --git a/tracker/choices.py b/tracker/choices.py
index f344087..11ea6d1 100644
--- a/tracker/choices.py
+++ b/tracker/choices.py
@@ -1,3 +1,6 @@
+"""
+A `tracker.choices` bodule that contains all model choices
+"""
from django.db import models
diff --git a/tracker/forms.py b/tracker/forms.py
index 2cfaddc..17f2abe 100644
--- a/tracker/forms.py
+++ b/tracker/forms.py
@@ -1,3 +1,6 @@
+"""
+A `tracker.forms` module that contains all forms
+"""
from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction
@@ -34,6 +37,7 @@ class SignUpForm(forms.ModelForm):
role = forms.ChoiceField(choices=Roles.choices)
class Meta:
+ """A metaclass for SignUpForm"""
model = CustomUser
fields = ("email", "password")
diff --git a/tracker/management/commands/run_telegram_bot.py b/tracker/management/commands/run_telegram_bot.py
index 8006a16..d3e6e51 100644
--- a/tracker/management/commands/run_telegram_bot.py
+++ b/tracker/management/commands/run_telegram_bot.py
@@ -1,3 +1,7 @@
+"""
+A `tracker.management.commands.run_telegram_bot` command for running telegram bots.`
+"""
+
import asyncio
from django.core.management.base import BaseCommand
@@ -12,20 +16,18 @@ class Command(BaseCommand):
This command initializes and starts the Telegram bot using asynchronous operations.
Methods:
- - handle(self, *args, **kwargs): Handles the execution of the command.
+ - handle(self, *args, **options): Handles the execution of the command.
"""
help = "Runs the Telegram bot"
- def handle(self, *args, **kwargs) -> None:
+ def handle(self, *args, **options) -> None:
"""
Handles the execution of the command.
This method is called when the management command is run. It starts the Telegram bot
and outputs a message to the standard output indicating that the bot has started.
- :param args: Additional positional arguments.
- :param kwargs: Additional keyword arguments.
:return: None
"""
self.stdout.write("Starting Telegram bot...")
diff --git a/tracker/models.py b/tracker/models.py
index 8269f6b..6e68fda 100644
--- a/tracker/models.py
+++ b/tracker/models.py
@@ -1,3 +1,6 @@
+"""
+A `tracker.models` module that contains all models of the `tracker` app
+"""
import requests
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
@@ -8,7 +11,7 @@
from shared.models import AbstractModel
from tracker.choices import Roles
-from tracker.values import ROLE_MAX_CHARACTER_LENGTH, DefaultModelValues
+from tracker.values import ROLE_MAX_CHARACTER_LENGTH, DefaultModelValues, REQUEST_TIMEOUT
class CustomUserManager(BaseUserManager):
@@ -31,10 +34,10 @@ def create_user(
:return: CustomUser
"""
if email:
- try:
- validate_email(email)
- except ValidationError:
- raise ValueError("Invalid email format")
+ # try:
+ validate_email(email)
+ # except ValidationError:
+ # raise ValueError("Invalid email format")
if not role:
role = Roles.CONTRIBUTOR
@@ -135,6 +138,7 @@ def is_staff(self) -> models.BooleanField:
"""
return self.is_admin
+ @property
def is_project_lead(self) -> bool:
"""
Checks if the user is a project lead
@@ -169,6 +173,9 @@ class Repository(AbstractModel):
)
class Meta:
+ """
+ A metaclass for Repository model
+ """
verbose_name_plural = "Repositories"
def clean(self) -> None:
@@ -185,7 +192,7 @@ def clean(self) -> None:
raise ValidationError("Repository author must be in the link.")
try:
- response = requests.get(str(self.link))
+ response = requests.get(str(self.link), timeout=REQUEST_TIMEOUT)
response.raise_for_status()
if not response.ok:
@@ -268,6 +275,7 @@ class Contributor(AbstractModel):
rank = models.IntegerField(default=0)
class Meta:
+ """A metaclass for Contributor model"""
verbose_name_plural = "Contributors"
def __str__(self):
diff --git a/tracker/tasks.py b/tracker/tasks.py
index 0bbea66..83ac928 100644
--- a/tracker/tasks.py
+++ b/tracker/tasks.py
@@ -1,3 +1,6 @@
+"""
+A `tracker.tasks` module that contains all celery tasks.
+"""
from asgiref.sync import async_to_sync
from celery import shared_task
diff --git a/tracker/telegram/__init__.py b/tracker/telegram/__init__.py
index 8b13789..e69de29 100644
--- a/tracker/telegram/__init__.py
+++ b/tracker/telegram/__init__.py
@@ -1 +0,0 @@
-
diff --git a/tracker/telegram/bot.py b/tracker/telegram/bot.py
index bf9dc81..fdb0a42 100644
--- a/tracker/telegram/bot.py
+++ b/tracker/telegram/bot.py
@@ -1,3 +1,7 @@
+"""
+A `tracker.telegram.bot` module that contains bot's logc
+"""
+
import asyncio
import logging
import os
@@ -156,7 +160,12 @@ async def send_available_issues(msg: Message) -> None:
@dp.message(F.text.contains("/issues "))
-async def get_contributor_tasks(message: Message):
+async def get_contributor_tasks(message: Message) -> None:
+ """
+ A function that sends all contributor issues
+ :param message: Message
+ :return: None
+ """
_, username = message.text.split(" ", 1)
regex = r"odhack"
@@ -181,26 +190,18 @@ async def send_revision_messages(telegram_id: str, reviews_data: list[dict]) ->
:params tele_id: The telegram user id of the user to send to
:reviews_data: A list of all the reviews data for all pull requests associated to the user repos
"""
- # TODO move it to `templates.py`
message = (
"=" * 50 + "\n" + "Revisions and Approvals" + "\n" + "=" * 50 + "\n\n"
)
for data in reviews_data:
- message += (
- "-------------------------------"
- f"Repo: {data['repo']}"
- "\n"
- f"Pull Request: {data['pull']}/"
- "\n"
- f"Reviews:"
- "\n"
+ message += TEMPLATES.review_data.substitute(
+ repository=data.get("repo", "Unknown"),
+ pull_request=data.get("pull", "Unknown"),
)
for review in data["reviews"]:
- message += (
- f"User: {review['user']['login']}"
- "\n"
- f"State: {review['state']}"
- "\n\n"
+ message += TEMPLATES.review_unit.substitute(
+ user=review.get("user", {}).get("login", "Unknown"),
+ state=review.get("state", "Unknown"),
)
message += "-------------------------------"
@@ -256,6 +257,11 @@ def main_button_markup() -> ReplyKeyboardMarkup:
async def create_tg_link(uuid) -> str:
+ """
+ A function that creates a telegram link
+ :param uuid: UUID
+ :return: str
+ """
return await create_start_link(bot=bot, payload=uuid, encode=True)
diff --git a/tracker/telegram/templates.py b/tracker/telegram/templates.py
index 63ebf05..23e6751 100644
--- a/tracker/telegram/templates.py
+++ b/tracker/telegram/templates.py
@@ -1,9 +1,13 @@
+"""
+A `tracker.telegram.templates` model for handling string templates
+"""
+
from dataclasses import dataclass
from string import Template
@dataclass
-class TemplateNames:
+class TemplateNames: # pylint: disable=too-many-instance-attributes
"""Class to hold all the templates used in the bot."""
greeting: Template
@@ -15,6 +19,8 @@ class TemplateNames:
issue_list_item: Template
support_contact: Template
no_support: Template
+ review_data: Template
+ review_unit: Template
TEMPLATES = TemplateNames(
@@ -44,4 +50,14 @@ class TemplateNames:
),
support_contact=Template("Support Contact:\n$repo_message\n$support_link"),
no_support=Template("Support Contact:\n$repo_message\nNo support provided."),
+ review_data=Template(
+ "-------------------------------"
+ "Repo: $repository"
+ "\n"
+ "Pull Request: $pull_request/"
+ "\n"
+ "Reviews:"
+ "\n"
+ ),
+ review_unit=Template("User: $user\nState: $state\n\n"),
)
diff --git a/tracker/tests/test_models.py b/tracker/tests/test_models.py
index 65515c2..1a776a4 100644
--- a/tracker/tests/test_models.py
+++ b/tracker/tests/test_models.py
@@ -3,6 +3,7 @@
django.setup()
from django.test import TestCase
+from django.core.exceptions import ValidationError
from faker import Faker
from tracker.choices import Roles
@@ -48,39 +49,37 @@ def test_create_user_with_invalid_email(self):
"""Test creating a user with invalid email format."""
invalid_email = "invalid.email@format"
- with self.assertRaises(ValueError) as context:
+ with self.assertRaises(ValidationError):
CustomUser.objects.create_user(
email=invalid_email, password=self.password, role=self.role
)
- self.assertEqual(str(context.exception), "Invalid email format")
class TestCustomUser(TestCase):
def setUp(self):
"""Set up test data."""
self.email = fake.email()
+ self.password = fake.password()
self.is_active = True
self.is_admin = True
- self.role = Roles.CONTRIBUTOR
+ self.role = Roles.PROJECT_LEAD
def test_str_method(self):
"""Test the __str__ method of the CustomUser model."""
user = CustomUser.objects.create_user(
email=self.email,
- is_active=self.is_active,
- is_admin=self.is_admin,
role=self.role,
+ password=self.password,
)
-
+
self.assertEqual(str(user), self.email)
def test_has_perm(self):
"""Test the has_perm method of the CustomUser model."""
user = CustomUser.objects.create_user(
email=self.email,
- is_active=self.is_active,
- is_admin=self.is_admin,
role=self.role,
+ password=self.password,
)
self.assertTrue(CustomUser.has_perm(user))
@@ -89,31 +88,28 @@ def test_has_module_perms(self):
"""Test the has_module_perms method of the CustomUser model."""
user = CustomUser.objects.create_user(
email=self.email,
- is_active=self.is_active,
- is_admin=self.is_admin,
role=self.role,
+ password=self.password,
)
self.assertTrue(CustomUser.has_module_perms(user))
def test_is_staff(self):
"""Test the is_staff property of the CustomUser model."""
- user = CustomUser.objects.create_user(
+ user = CustomUser.objects.create_superuser(
email=self.email,
- is_active=self.is_active,
- is_admin=self.is_admin,
role=self.role,
+ password=self.password,
)
- self.assertEqual(user.is_staff, self.is_admin)
+ self.assertTrue(user.is_staff)
def test_is_project_lead(self):
"""Test the is_project_lead method of the CustomUser model."""
user = CustomUser.objects.create_user(
email=self.email,
- is_active=self.is_active,
- is_admin=self.is_admin,
role=self.role,
+ password=self.password,
)
- self.assertEqual(user.is_project_lead, self.role == Roles.PROJECT_LEAD)
+ self.assertTrue(user.is_project_lead)
diff --git a/tracker/tests/test_utils.py b/tracker/tests/test_utils.py
index 944d920..2398774 100644
--- a/tracker/tests/test_utils.py
+++ b/tracker/tests/test_utils.py
@@ -14,9 +14,7 @@
get_user,
)
-# TODO: move it to `values.py` in `tracker/tests`
fake = Faker()
-# TODO: move it to `values.py` in `tracker/tests`
telagram_id = fake.random_int(min=100000000, max=9999999999)
@@ -152,7 +150,6 @@ def test_missing_events_url(self, mock_get):
self.assertEqual(result, {})
-
@patch("requests.get")
def test_malformed_response(self, mock_get):
"""Test handling of malformed response data."""
diff --git a/tracker/urls.py b/tracker/urls.py
index a5cd151..0089cd9 100644
--- a/tracker/urls.py
+++ b/tracker/urls.py
@@ -1,3 +1,7 @@
+"""
+A `tracker.urls` module provides URL patterns for the tracker app.
+"""
+
from django.urls import path
from .views import CreateUserView
diff --git a/tracker/utils.py b/tracker/utils.py
index c508cd6..67d78fa 100644
--- a/tracker/utils.py
+++ b/tracker/utils.py
@@ -1,3 +1,7 @@
+"""
+A `tracker.utils` module provides utility functions for tracking.
+"""
+
import logging
import re
from collections import defaultdict
@@ -15,6 +19,7 @@
PULLS_REVIEWS_URL,
PULLS_URL,
SECONDS_IN_AN_HOUR,
+ REQUEST_TIMEOUT,
)
logger = logging.getLogger(__name__)
@@ -46,7 +51,7 @@ def get_all_repostitories(tele_id: str) -> list[dict]:
if telegram_user:
return list(telegram_user.user.repository_set.values())
- return list()
+ return []
@sync_to_async
@@ -87,13 +92,17 @@ def check_issue_assignment_events(issue: dict) -> dict:
:param issue: The issue dictionary containing information about the issue, including
an "events_url" to fetch assignment events.
:return: A dictionary with two keys:
- - "assignee": the login of the user assigned to the issue (empty string if not assigned).
- - "assigned_at": the time the issue was assigned (empty string if no assignment event).
+ - "assignee": the login of the user assigned to the issue
+ (empty string if not assigned).
+ - "assigned_at": the time the issue was assigned
+ (empty string if no assignment event).
"""
try:
events_url = issue.get("events_url", str())
- response = requests.get(events_url, headers=HEADERS)
+ response = requests.get(
+ events_url, headers=HEADERS, timeout=REQUEST_TIMEOUT
+ )
response.raise_for_status()
events = response.json()
@@ -123,7 +132,7 @@ def get_all_open_and_assigned_issues(url: str) -> list[dict]:
:return: A list of dictionaries representing open and assigned issues.
"""
try:
- response = requests.get(url, headers=HEADERS)
+ response = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
issues = response.json()
@@ -156,7 +165,9 @@ def get_all_open_pull_requests(url: str) -> list[dict]:
:return: A list of dictionaries representing open pull requests.
"""
try:
- response = requests.get(url, headers=HEADERS, params={"state": "open"})
+ response = requests.get(
+ url, headers=HEADERS, params={"state": "open"}, timeout=REQUEST_TIMEOUT
+ )
response.raise_for_status()
response = response.json()
@@ -182,7 +193,7 @@ def get_issues_without_pull_requests(
for issue in issues:
issue["assignment_info"] = check_issue_assignment_events(issue)
- assigned_at = issue.get("assignment_info", dict()).get("assigned_at")
+ assigned_at = issue.get("assignment_info", {}).get("assigned_at")
time_delta = (
relativedelta(
@@ -198,17 +209,17 @@ def get_issues_without_pull_requests(
pull_requests = get_all_open_pull_requests(pull_requests_url)
pull_requests_users = [
- pull_request.get("user", dict()).get("login")
+ pull_request.get("user", {}).get("login")
for pull_request in pull_requests
- if pull_request.get("user", dict()).get("login")
+ if pull_request.get("user", {}).get("login")
]
- result = list()
+ result = []
for issue in issues.copy():
if (
- issue.get("days", 0) >= 1 # TODO make it correspond to repository settings
- and issue.get("assignee", dict()).get("login") not in pull_requests_users
+ issue.get("days", 0) >= 1 # TODO make it correspond to repository settings
+ and issue.get("assignee", {}).get("login") not in pull_requests_users
):
result.append(issue)
@@ -218,13 +229,15 @@ def get_issues_without_pull_requests(
def get_all_available_issues(url: str) -> list[dict]:
"""
Retrieves all available issues from a given URL.
- If the response status is not successful, it raises an exception and returns an empty list.
+ If the response status is not successful,
+ it raises an exception and returns an empty list.
:param url: The API endpoint for issues.
- :return: A list of dictionaries representing available issues or an empty list if an error occurs.
+ :return: A list of dictionaries representing available
+ issues or an empty list if an error occurs.
"""
try:
- response = requests.get(url, headers=HEADERS)
+ response = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
issues = response.json()
@@ -257,7 +270,7 @@ def get_pull_reviews(url: str) -> list[dict]:
:return: A list of dictionaries representing available issues.
"""
try:
- response = requests.get(url, headers=HEADERS)
+ response = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
if response.ok:
@@ -306,7 +319,7 @@ def get_contributor_issues(
api_url = ISSUES_SEARCH.format(username=username)
try:
- response = requests.get(api_url, headers=HEADERS)
+ response = requests.get(api_url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
issues = response.json().get("items", [])
@@ -345,6 +358,11 @@ def attach_link_to_issue(issue: dict) -> str:
def get_repository_from_issue(issue: dict) -> dict:
+ """
+ Retrieves the repository name from an issue.
+ :param issue: dict
+ :return: dict
+ """
repository_url = issue.get("repository_url", "")
if repository_url:
parts = repository_url.rstrip("/").split("/")
@@ -386,9 +404,12 @@ def get_time_before_deadline(issue: dict) -> str:
if deadline_datetime > now:
remaining_time = deadline_datetime - now
- return f"Time remaining: {remaining_time.days} days, {remaining_time.seconds // SECONDS_IN_AN_HOUR} hours"
- else:
- return "Deadline has passed."
+ return (
+ f"Time remaining: {remaining_time.days} days, "
+ f"{remaining_time.seconds // SECONDS_IN_AN_HOUR} hours"
+ )
+
+ return "Deadline has passed."
def get_support_link(telegram_username: str) -> str:
@@ -419,5 +440,7 @@ def get_repository_support(author: str, repo_name: str) -> "Support":
repository = Repository.objects.filter(author=author, name=repo_name).first()
if repository:
- return Support.objects.filter(user=repository.user, repository=repository).first()
+ return Support.objects.filter(
+ user=repository.user, repository=repository
+ ).first()
return None
diff --git a/tracker/values.py b/tracker/values.py
index 1d3cb1d..227ed8f 100644
--- a/tracker/values.py
+++ b/tracker/values.py
@@ -1,3 +1,6 @@
+"""
+A `tracker.values` model that contains all hardcoded values for tracking purposes
+"""
import os
from dataclasses import dataclass
@@ -5,6 +8,7 @@
load_dotenv()
+REQUEST_TIMEOUT: int = 10 # in seconds
ISSUES_URL: str = "https://api.github.com/repos/{owner}/{repo}/issues"
PULLS_URL: str = "https://api.github.com/repos/{owner}/{repo}/pulls"
PULLS_REVIEWS_URL: str = (
diff --git a/tracker/views.py b/tracker/views.py
index fa9e6f7..a4dc800 100644
--- a/tracker/views.py
+++ b/tracker/views.py
@@ -1,3 +1,7 @@
+"""
+A `tracker.views` model that contains all views
+"""
+
from django.contrib import messages
from django.contrib.auth import login
from django.http import HttpResponse
@@ -7,7 +11,11 @@
from .forms import SignUpForm
-class CreateUserView(CreateView):
+class CreateUserView(CreateView): # pylint: disable=too-many-ancestors
+ """
+ A user creation view
+ """
+
form_class = SignUpForm
success_url = "/admin/"
template_name = "signup.html"
@@ -40,6 +48,7 @@ def post(self, request, *args, **kwargs) -> HttpResponse:
return redirect(self.success_url)
- [messages.error(request, error_) for error_ in form.errors.values()]
+ for error_ in form.errors.values():
+ messages.error(request, error_)
return render(request, self.template_name, {"form": form})