diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..7f22e63 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,663 @@ +[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 + +# 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.9 + +# 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=100 + +# 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, + missing-module-docstring, + missing-function-docstring, + missing-class-docstring, + no-member, + redefined-builtin, + too-many-ancestors, + too-few-public-methods, + unused-argument, + too-few-public-methods, + arguments-differ, + invalid-overridden-method, + unsupported-binary-operation + +# 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, + TODO + +# 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 + +# Ignore migrations file +[MASTER] +ignore=migrations \ No newline at end of file diff --git a/accounts/apps.py b/accounts/apps.py index 5f93240..e9ee8c6 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -1,14 +1,13 @@ from django.apps import AppConfig +from django.db.models.signals import post_save +from .models import User +from .signals import post_save_account_receiver class AccountsConfig(AppConfig): name = "accounts" def ready(self) -> None: - from django.db.models.signals import post_save - from .models import User - from .signals import post_save_account_receiver - post_save.connect(post_save_account_receiver, sender=User) return super().ready() diff --git a/accounts/decorators.py b/accounts/decorators.py index 12aeded..97059b3 100644 --- a/accounts/decorators.py +++ b/accounts/decorators.py @@ -1,5 +1,3 @@ -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.decorators import user_passes_test from django.shortcuts import redirect @@ -21,9 +19,8 @@ def admin_required( if test_func(request.user): # Call the original function if the user passes the test return function(request, *args, **kwargs) if function else None - else: - # Redirect to the specified URL if the user fails the test - return redirect(redirect_to) + # Redirect to the specified URL if the user fails the test + return redirect(redirect_to) return wrapper if function else test_func @@ -46,9 +43,8 @@ def lecturer_required( if test_func(request.user): # Call the original function if the user passes the test return function(request, *args, **kwargs) if function else None - else: - # Redirect to the specified URL if the user fails the test - return redirect(redirect_to) + # Redirect to the specified URL if the user fails the test + return redirect(redirect_to) return wrapper if function else test_func @@ -71,8 +67,7 @@ def student_required( if test_func(request.user): # Call the original function if the user passes the test return function(request, *args, **kwargs) if function else None - else: - # Redirect to the specified URL if the user fails the test - return redirect(redirect_to) + # Redirect to the specified URL if the user fails the test + return redirect(redirect_to) return wrapper if function else test_func diff --git a/accounts/signals.py b/accounts/signals.py index b549fe0..9194683 100644 --- a/accounts/signals.py +++ b/accounts/signals.py @@ -5,7 +5,7 @@ from .utils import ( ) -def post_save_account_receiver(sender, instance=None, created=False, *args, **kwargs): +def post_save_account_receiver(instance=None, created=False): """ Send email notification """ diff --git a/accounts/tests.py b/accounts/tests.py index 9d1ecae..2177ffb 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -5,38 +5,37 @@ from accounts.decorators import admin_required User = get_user_model() + class AdminRequiredDecoratorTests(TestCase): def setUp(self): self.superuser = User.objects.create_superuser( - username='admin', email='admin@example.com', password='password' + username="admin", email="admin@example.com", password="password" ) self.user = User.objects.create_user( - username='user', email='user@example.com', password='password' + username="user", email="user@example.com", password="password" ) self.factory = RequestFactory() - + def admin_view(self, request): return HttpResponse() def test_admin_required_decorator(self): # Apply the admin_required decorator to the view function decorated_view = admin_required(self.admin_view) - + request = self.factory.get("/") request.user = self.user response = decorated_view(request) self.assertEqual(response.status_code, 302) - def test_admin_required_decorator_with_redirect(self): # Apply the admin_required decorator to the view function - decorated_view = admin_required(function=self.admin_view,redirect_to="/login/") - + decorated_view = admin_required(function=self.admin_view, redirect_to="/login/") + request = self.factory.get("/") request.user = self.user response = decorated_view(request) # Assert redirection to login page self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/login/') - + self.assertEqual(response.url, "/login/") diff --git a/accounts/tests/test_decorators.py b/accounts/tests/test_decorators.py index 9326626..5882a8b 100644 --- a/accounts/tests/test_decorators.py +++ b/accounts/tests/test_decorators.py @@ -5,51 +5,51 @@ from accounts.decorators import admin_required, lecturer_required, student_requi User = get_user_model() + class AdminRequiredDecoratorTests(TestCase): def setUp(self): self.superuser = User.objects.create_superuser( - username='admin', email='admin@example.com', password='password' + username="admin", email="admin@example.com", password="password" ) self.user = User.objects.create_user( - username='user', email='user@example.com', password='password' + username="user", email="user@example.com", password="password" ) self.factory = RequestFactory() - + def admin_view(self, request): return HttpResponse("Admin View Content") def test_admin_required_decorator_redirects(self): decorated_view = admin_required(self.admin_view) - + request = self.factory.get("/restricted-view") request.user = self.user response = decorated_view(request) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/") - def test_admin_required_decorator_redirects_to_correct_path(self): - decorated_view = admin_required(function=self.admin_view,redirect_to="/login/") - + decorated_view = admin_required(function=self.admin_view, redirect_to="/login/") + request = self.factory.get("restricted-view") request.user = self.user response = decorated_view(request) self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/login/') - + self.assertEqual(response.url, "/login/") + def test_admin_required_decorator_does_not_redirect_superuser(self): decorated_view = admin_required(self.admin_view) - + request = self.factory.get("/restricted-view") request.user = self.superuser response = decorated_view(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Admin View Content") - + def test_admin_redirect_decorator_return_correct_response(self): decorated_view = admin_required(self.admin_view) - + request = self.factory.get("/restricted-view") request.user = self.superuser response = decorated_view(request) @@ -59,10 +59,13 @@ class AdminRequiredDecoratorTests(TestCase): class LecturerRequiredDecoratorTests(TestCase): def setUp(self): self.lecturer = User.objects.create_user( - username='lecturer', email='lecturer@example.com', password='password', is_lecturer=True + username="lecturer", + email="lecturer@example.com", + password="password", + is_lecturer=True, ) self.user = User.objects.create_user( - username='user', email='user@example.com', password='password' + username="user", email="user@example.com", password="password" ) self.factory = RequestFactory() @@ -81,7 +84,9 @@ class LecturerRequiredDecoratorTests(TestCase): self.assertEqual(response.url, "/") def test_lecturer_required_decorator_redirects_to_correct_path(self): - decorated_view = lecturer_required(function=self.lecturer_view, redirect_to="/login/") + decorated_view = lecturer_required( + function=self.lecturer_view, redirect_to="/login/" + ) request = self.factory.get("/restricted-view") request.user = self.user @@ -89,7 +94,7 @@ class LecturerRequiredDecoratorTests(TestCase): response = decorated_view(request) self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/login/') + self.assertEqual(response.url, "/login/") def test_lecturer_required_decorator_does_not_redirect_lecturer(self): decorated_view = lecturer_required(self.lecturer_view) @@ -112,13 +117,17 @@ class LecturerRequiredDecoratorTests(TestCase): self.assertIsInstance(response, HttpResponse) + class StudentRequiredDecoratorTests(TestCase): def setUp(self): self.student = User.objects.create_user( - username='student', email='student@example.com', password='password', is_student=True + username="student", + email="student@example.com", + password="password", + is_student=True, ) self.user = User.objects.create_user( - username='user', email='user@example.com', password='password' + username="user", email="user@example.com", password="password" ) self.factory = RequestFactory() @@ -137,7 +146,9 @@ class StudentRequiredDecoratorTests(TestCase): self.assertEqual(response.url, "/") def test_student_required_decorator_redirects_to_correct_path(self): - decorated_view = student_required(function=self.student_view, redirect_to="/login/") + decorated_view = student_required( + function=self.student_view, redirect_to="/login/" + ) request = self.factory.get("/restricted-view") request.user = self.user @@ -145,7 +156,7 @@ class StudentRequiredDecoratorTests(TestCase): response = decorated_view(request) self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/login/') + self.assertEqual(response.url, "/login/") def test_student_required_decorator_does_not_redirect_student(self): decorated_view = student_required(self.student_view) @@ -166,4 +177,4 @@ class StudentRequiredDecoratorTests(TestCase): response = decorated_view(request) - self.assertIsInstance(response, HttpResponse) \ No newline at end of file + self.assertIsInstance(response, HttpResponse) diff --git a/accounts/tests/test_filters.py b/accounts/tests/test_filters.py index 9405116..fcca755 100644 --- a/accounts/tests/test_filters.py +++ b/accounts/tests/test_filters.py @@ -1,14 +1,30 @@ from django.test import TestCase -from accounts.filters import LecturerFilter, StudentFilter +from accounts.filters import LecturerFilter, StudentFilter from accounts.models import User, Student from course.models import Program + class LecturerFilterTestCase(TestCase): def setUp(self): - User.objects.create(username="user1", first_name="John", last_name="Doe", email="john@example.com") - User.objects.create(username="user2", first_name="Jane", last_name="Doe", email="jane@example.com") - User.objects.create(username="user3", first_name="Alice", last_name="Smith", email="alice@example.com") - + User.objects.create( + username="user1", + first_name="John", + last_name="Doe", + email="john@example.com", + ) + User.objects.create( + username="user2", + first_name="Jane", + last_name="Doe", + email="jane@example.com", + ) + User.objects.create( + username="user3", + first_name="Alice", + last_name="Smith", + email="alice@example.com", + ) + def test_username_filter(self): filter_set = LecturerFilter(data={"username": "user1"}) self.assertEqual(len(filter_set.qs), 1) @@ -19,36 +35,80 @@ class LecturerFilterTestCase(TestCase): def test_email_filter(self): filter_set = LecturerFilter(data={"email": "example.com"}) - self.assertEqual(len(filter_set.qs), 3) # All users should be returned since all have email addresses with "example.com" + self.assertEqual( + len(filter_set.qs), 3 + ) # All users should be returned since all have email addresses with "example.com" def test_combined_filters(self): filter_set = LecturerFilter(data={"name": "Doe", "email": "example.com"}) - self.assertEqual(len(filter_set.qs), 2) # Both John Doe and Jane Doe should be returned + self.assertEqual( + len(filter_set.qs), 2 + ) # Both John Doe and Jane Doe should be returned filter_set = LecturerFilter(data={"name": "Alice", "email": "example.com"}) - self.assertEqual(len(filter_set.qs), 1) # 1 user matches Alice with "example.com" in the email + self.assertEqual( + len(filter_set.qs), 1 + ) # 1 user matches Alice with "example.com" in the email def test_no_filters(self): filter_set = LecturerFilter(data={}) - self.assertEqual(len(filter_set.qs), 3) # All users should be returned since no filters are applied + self.assertEqual( + len(filter_set.qs), 3 + ) # All users should be returned since no filters are applied + class StudentFilterTestCase(TestCase): def setUp(self): - program1 = Program.objects.create(title="Computer Science", summary="Program for computer science students") - program2 = Program.objects.create(title="Mathematics", summary="Program for mathematics students") - program3 = Program.objects.create(title="Computer Engineering", summary="Program for computer engineering students") + program1 = Program.objects.create( + title="Computer Science", summary="Program for computer science students" + ) + program2 = Program.objects.create( + title="Mathematics", summary="Program for mathematics students" + ) + program3 = Program.objects.create( + title="Computer Engineering", + summary="Program for computer engineering students", + ) - Student.objects.create(student=User.objects.create(username="student1", first_name="John", last_name="Doe", email="john@example.com"), program=program1) - Student.objects.create(student=User.objects.create(username="student2", first_name="Jane", last_name="Williams", email="jane@example.com"), program=program2) - Student.objects.create(student=User.objects.create(username="student3", first_name="Alice", last_name="Smith", email="alice@example.com"), program=program3) + Student.objects.create( + student=User.objects.create( + username="student1", + first_name="John", + last_name="Doe", + email="john@example.com", + ), + program=program1, + ) + Student.objects.create( + student=User.objects.create( + username="student2", + first_name="Jane", + last_name="Williams", + email="jane@example.com", + ), + program=program2, + ) + Student.objects.create( + student=User.objects.create( + username="student3", + first_name="Alice", + last_name="Smith", + email="alice@example.com", + ), + program=program3, + ) def test_name_filter(self): - filtered_students = StudentFilter(data = {'name': 'John'}, queryset=Student.objects.all()).qs + filtered_students = StudentFilter( + data={"name": "John"}, queryset=Student.objects.all() + ).qs self.assertEqual(filtered_students.count(), 1) - + def test_email_filter(self): filter_set = StudentFilter(data={"email": "example.com"}) - self.assertEqual(len(filter_set.qs), 3) # All students should be returned since all have email addresses with "example.com" + self.assertEqual( + len(filter_set.qs), 3 + ) # All students should be returned since all have email addresses with "example.com" def test_program_filter(self): filter_set = StudentFilter(data={"program__title": "Computer Science"}) diff --git a/accounts/utils.py b/accounts/utils.py index fc0619c..ef5af43 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -1,7 +1,7 @@ +import threading from datetime import datetime from django.contrib.auth import get_user_model from django.conf import settings -import threading from core.utils import send_html_email diff --git a/accounts/views.py b/accounts/views.py index 823adda..910df56 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,34 +1,48 @@ -from django.http.response import JsonResponse -from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages -from django.contrib.auth.decorators import login_required from django.contrib.auth import update_session_auth_hash -from django.views.generic import CreateView, ListView -from django.db.models import Q -from django.utils.decorators import method_decorator +from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordChangeForm +from django.http import HttpResponse, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.template.loader import get_template, render_to_string +from django.utils.decorators import method_decorator +from django.views.generic import CreateView from django_filters.views import FilterView -from core.models import Session, Semester -from course.models import Course -from result.models import TakenCourse -from .decorators import admin_required -from .forms import ( +from xhtml2pdf import pisa + +from accounts.decorators import admin_required +from accounts.filters import LecturerFilter, StudentFilter +from accounts.forms import ( + ParentAddForm, + ProfileUpdateForm, + ProgramUpdateForm, StaffAddForm, StudentAddForm, - ProfileUpdateForm, - ParentAddForm, - ProgramUpdateForm, ) -from .models import User, Student, Parent -from .filters import LecturerFilter, StudentFilter +from accounts.models import Parent, Student, User +from core.models import Semester, Session +from course.models import Course +from result.models import TakenCourse -# to generate pdf from template we need the following -from django.http import HttpResponse -from django.template.loader import get_template # to get template which render as pdf -from xhtml2pdf import pisa -from django.template.loader import ( - render_to_string, -) # to render a template into a string +# ######################################################## +# Utility Functions +# ######################################################## + + +def render_to_pdf(template_name, context): + """Render a given template to PDF format.""" + response = HttpResponse(content_type="application/pdf") + response["Content-Disposition"] = 'filename="profile.pdf"' + template = render_to_string(template_name, context) + pdf = pisa.CreatePDF(template, dest=response) + if pdf.err: + return HttpResponse("We had some problems generating the PDF") + return response + + +# ######################################################## +# Authentication and Registration +# ######################################################## def validate_username(request): @@ -42,193 +56,125 @@ def register(request): form = StudentAddForm(request.POST) if form.is_valid(): form.save() - messages.success(request, f"Account created successfuly.") - else: - messages.error( - request, f"Somthing is not correct, please fill all fields correctly." - ) + messages.success(request, "Account created successfully.") + return redirect("login") + messages.error( + request, "Something is not correct, please fill all fields correctly." + ) else: - form = StudentAddForm(request.POST) + form = StudentAddForm() return render(request, "registration/register.html", {"form": form}) +# ######################################################## +# Profile Views +# ######################################################## + + @login_required def profile(request): - """Show profile of any user that fire out the request""" + """Show profile of the current user.""" current_session = Session.objects.filter(is_current_session=True).first() current_semester = Semester.objects.filter( is_current_semester=True, session=current_session ).first() + context = { + "title": request.user.get_full_name, + "current_session": current_session, + "current_semester": current_semester, + } + if request.user.is_lecturer: courses = Course.objects.filter( - allocated_course__lecturer__pk=request.user.id - ).filter(semester=current_semester) - return render( - request, - "accounts/profile.html", - { - "title": request.user.get_full_name, - "courses": courses, - "current_session": current_session, - "current_semester": current_semester, - }, + allocated_course__lecturer__pk=request.user.id, semester=current_semester ) - elif request.user.is_student: - level = Student.objects.get(student__pk=request.user.id) - try: - parent = Parent.objects.get(student=level) - except: - parent = "no parent set" - courses = TakenCourse.objects.filter( - student__student__id=request.user.id, course__level=level.level - ) - context = { - "title": request.user.get_full_name, - "parent": parent, - "courses": courses, - "level": level, - "current_session": current_session, - "current_semester": current_semester, - } + context["courses"] = courses return render(request, "accounts/profile.html", context) - else: - staff = User.objects.filter(is_lecturer=True) - return render( - request, - "accounts/profile.html", - { - "title": request.user.get_full_name, - "staff": staff, - "current_session": current_session, - "current_semester": current_semester, - }, + + if request.user.is_student: + student = get_object_or_404(Student, student__pk=request.user.id) + parent = Parent.objects.filter(student=student).first() + courses = TakenCourse.objects.filter( + student__student__id=request.user.id, course__level=student.level ) + context.update( + { + "parent": parent, + "courses": courses, + "level": student.level, + } + ) + return render(request, "accounts/profile.html", context) - -# function that generate pdf by taking Django template and its context, -def render_to_pdf(template_name, context): - """Renders a given template to PDF format.""" - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="profile.pdf"' # Set default filename - - template = render_to_string(template_name, context) - pdf = pisa.CreatePDF(template, dest=response) - if pdf.err: - return HttpResponse("We had some problems generating the PDF") - - return response + # For superuser or other staff + staff = User.objects.filter(is_lecturer=True) + context["staff"] = staff + return render(request, "accounts/profile.html", context) @login_required @admin_required -def profile_single(request, id): - """Show profile of any selected user""" - if request.user.id == id: - return redirect("/profile/") +def profile_single(request, user_id): + """Show profile of any selected user.""" + if request.user.id == user_id: + return redirect("profile") current_session = Session.objects.filter(is_current_session=True).first() current_semester = Semester.objects.filter( is_current_semester=True, session=current_session ).first() + user = get_object_or_404(User, pk=user_id) - user = User.objects.get(pk=id) - """ - If download_pdf exists, instead of calling render_to_pdf directly, - pass the context dictionary built for the specific user type - (lecturer, student, or superuser) to the render_to_pdf function. - """ - if request.GET.get("download_pdf"): - if user.is_lecturer: - courses = Course.objects.filter(allocated_course__lecturer__pk=id).filter( - semester=current_semester - ) - context = { - "title": user.get_full_name, - "user": user, + context = { + "title": user.get_full_name, + "user": user, + "current_session": current_session, + "current_semester": current_semester, + } + + if user.is_lecturer: + courses = Course.objects.filter( + allocated_course__lecturer__pk=user_id, semester=current_semester + ) + context.update( + { "user_type": "Lecturer", "courses": courses, - "current_session": current_session, - "current_semester": current_semester, } - elif user.is_student: - student = Student.objects.get(student__pk=id) - courses = TakenCourse.objects.filter( - student__student__id=id, course__level=student.level - ) - context = { - "title": user.get_full_name, - "user": user, - "user_type": "student", + ) + elif user.is_student: + student = get_object_or_404(Student, student__pk=user_id) + courses = TakenCourse.objects.filter( + student__student__id=user_id, course__level=student.level + ) + context.update( + { + "user_type": "Student", "courses": courses, "student": student, - "current_session": current_session, - "current_semester": current_semester, - } - else: - context = { - "title": user.get_full_name, - "user": user, - "user_type": "superuser", - "current_session": current_session, - "current_semester": current_semester, } + ) + else: + context["user_type"] = "Superuser" + + if request.GET.get("download_pdf"): return render_to_pdf("pdf/profile_single.html", context) - else: - if user.is_lecturer: - courses = Course.objects.filter(allocated_course__lecturer__pk=id).filter( - semester=current_semester - ) - context = { - "title": user.get_full_name, - "user": user, - "user_type": "Lecturer", - "courses": courses, - "current_session": current_session, - "current_semester": current_semester, - } - return render(request, "accounts/profile_single.html", context) - elif user.is_student: - student = Student.objects.get(student__pk=id) - courses = TakenCourse.objects.filter( - student__student__id=id, course__level=student.level - ) - context = { - "title": user.get_full_name, - "user": user, - "user_type": "student", - "courses": courses, - "student": student, - "current_session": current_session, - "current_semester": current_semester, - } - return render(request, "accounts/profile_single.html", context) - else: - context = { - "title": user.get_full_name, - "user": user, - "user_type": "superuser", - "current_session": current_session, - "current_semester": current_semester, - } - return render(request, "accounts/profile_single.html", context) + return render(request, "accounts/profile_single.html", context) @login_required @admin_required def admin_panel(request): - return render( - request, "setting/admin_panel.html", {"title": request.user.get_full_name} - ) + return render(request, "setting/admin_panel.html", {"title": "Admin Panel"}) # ######################################################## +# Settings Views +# ######################################################## -# ######################################################## -# Setting views -# ######################################################## @login_required def profile_update(request): if request.method == "POST": @@ -237,18 +183,10 @@ def profile_update(request): form.save() messages.success(request, "Your profile has been updated successfully.") return redirect("profile") - else: - messages.error(request, "Please correct the error(s) below.") + messages.error(request, "Please correct the error(s) below.") else: form = ProfileUpdateForm(instance=request.user) - return render( - request, - "setting/profile_info_change.html", - { - "title": "Setting", - "form": form, - }, - ) + return render(request, "setting/profile_info_change.html", {"form": form}) @login_required @@ -260,20 +198,15 @@ def change_password(request): update_session_auth_hash(request, user) messages.success(request, "Your password was successfully updated!") return redirect("profile") - else: - messages.error(request, "Please correct the error(s) below. ") + messages.error(request, "Please correct the error(s) below.") else: form = PasswordChangeForm(request.user) - return render( - request, - "setting/password_change.html", - { - "form": form, - }, - ) + return render(request, "setting/password_change.html", {"form": form}) # ######################################################## +# Staff (Lecturer) Views +# ######################################################## @login_required @@ -281,58 +214,39 @@ def change_password(request): def staff_add_view(request): if request.method == "POST": form = StaffAddForm(request.POST) - first_name = request.POST.get("first_name") - last_name = request.POST.get("last_name") - email = request.POST.get("email") - if form.is_valid(): - - form.save() + lecturer = form.save() + full_name = lecturer.get_full_name + email = lecturer.email messages.success( request, - "Account for lecturer " - + first_name - + " " - + last_name - + " has been created. An email with account credentials will be sent to " - + email - + " within a minute.", + f"Account for lecturer {full_name} has been created. " + f"An email with account credentials will be sent to {email} within a minute.", ) return redirect("lecturer_list") else: form = StaffAddForm() - - context = { - "title": "Lecturer Add", - "form": form, - } - - return render(request, "accounts/add_staff.html", context) + return render( + request, "accounts/add_staff.html", {"title": "Add Lecturer", "form": form} + ) @login_required @admin_required def edit_staff(request, pk): - instance = get_object_or_404(User, is_lecturer=True, pk=pk) + lecturer = get_object_or_404(User, is_lecturer=True, pk=pk) if request.method == "POST": - form = ProfileUpdateForm(request.POST, request.FILES, instance=instance) - full_name = instance.get_full_name + form = ProfileUpdateForm(request.POST, request.FILES, instance=lecturer) if form.is_valid(): form.save() - - messages.success(request, "Lecturer " + full_name + " has been updated.") + full_name = lecturer.get_full_name + messages.success(request, f"Lecturer {full_name} has been updated.") return redirect("lecturer_list") - else: - messages.error(request, "Please correct the error below.") + messages.error(request, "Please correct the error below.") else: - form = ProfileUpdateForm(instance=instance) + form = ProfileUpdateForm(instance=lecturer) return render( - request, - "accounts/edit_lecturer.html", - { - "title": "Edit Lecturer", - "form": form, - }, + request, "accounts/edit_lecturer.html", {"title": "Edit Lecturer", "form": form} ) @@ -341,7 +255,7 @@ class LecturerFilterView(FilterView): filterset_class = LecturerFilter queryset = User.objects.filter(is_lecturer=True) template_name = "accounts/lecturer_list.html" - paginate_by = 10 # if pagination is desired + paginate_by = 10 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -349,107 +263,76 @@ class LecturerFilterView(FilterView): return context -# lecturers list pdf +@login_required +@admin_required def render_lecturer_pdf_list(request): lecturers = User.objects.filter(is_lecturer=True) template_path = "pdf/lecturer_list.html" context = {"lecturers": lecturers} - response = HttpResponse( - content_type="application/pdf" - ) # convert the response to pdf + response = HttpResponse(content_type="application/pdf") response["Content-Disposition"] = 'filename="lecturers_list.pdf"' - # find the template and render it. template = get_template(template_path) html = template.render(context) - # create a pdf pisa_status = pisa.CreatePDF(html, dest=response) - # if error then show some funny view if pisa_status.err: - return HttpResponse("We had some errors
" + html + "") + return HttpResponse(f"We had some errors
{html}")
return response
-# @login_required
-# @lecturer_required
-# def delete_staff(request, pk):
-# staff = get_object_or_404(User, pk=pk)
-# staff.delete()
-# return redirect('lecturer_list')
-
-
@login_required
@admin_required
def delete_staff(request, pk):
- lecturer = get_object_or_404(User, pk=pk)
+ lecturer = get_object_or_404(User, is_lecturer=True, pk=pk)
full_name = lecturer.get_full_name
lecturer.delete()
- messages.success(request, "Lecturer " + full_name + " has been deleted.")
+ messages.success(request, f"Lecturer {full_name} has been deleted.")
return redirect("lecturer_list")
# ########################################################
+# Student Views
+# ########################################################
-# ########################################################
-# Student views
-# ########################################################
@login_required
@admin_required
def student_add_view(request):
if request.method == "POST":
form = StudentAddForm(request.POST)
- first_name = request.POST.get("first_name")
- last_name = request.POST.get("last_name")
- email = request.POST.get("email")
if form.is_valid():
- form.save()
+ student = form.save()
+ full_name = student.get_full_name
+ email = student.email
messages.success(
request,
- "Account for "
- + first_name
- + " "
- + last_name
- + " has been created. An email with account credentials will be sent to "
- + email
- + " within a minute.",
+ f"Account for {full_name} has been created. "
+ f"An email with account credentials will be sent to {email} within a minute.",
)
return redirect("student_list")
- else:
- messages.error(request, "Correct the error(s) below.")
+ messages.error(request, "Correct the error(s) below.")
else:
form = StudentAddForm()
-
return render(
- request,
- "accounts/add_student.html",
- {"title": "Add Student", "form": form},
+ request, "accounts/add_student.html", {"title": "Add Student", "form": form}
)
@login_required
@admin_required
def edit_student(request, pk):
- # instance = User.objects.get(pk=pk)
- instance = get_object_or_404(User, is_student=True, pk=pk)
+ student_user = get_object_or_404(User, is_student=True, pk=pk)
if request.method == "POST":
- form = ProfileUpdateForm(request.POST, request.FILES, instance=instance)
- full_name = instance.get_full_name
+ form = ProfileUpdateForm(request.POST, request.FILES, instance=student_user)
if form.is_valid():
form.save()
-
- messages.success(request, ("Student " + full_name + " has been updated."))
+ full_name = student_user.get_full_name
+ messages.success(request, f"Student {full_name} has been updated.")
return redirect("student_list")
- else:
- messages.error(request, "Please correct the error below.")
+ messages.error(request, "Please correct the error below.")
else:
- form = ProfileUpdateForm(instance=instance)
+ form = ProfileUpdateForm(instance=student_user)
return render(
- request,
- "accounts/edit_student.html",
- {
- "title": "Edit-profile",
- "form": form,
- },
+ request, "accounts/edit_student.html", {"title": "Edit Student", "form": form}
)
@@ -466,23 +349,19 @@ class StudentListView(FilterView):
return context
-# student list pdf
+@login_required
+@admin_required
def render_student_pdf_list(request):
students = Student.objects.all()
template_path = "pdf/student_list.html"
context = {"students": students}
- response = HttpResponse(
- content_type="application/pdf"
- ) # convert the response to pdf
+ response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = 'filename="students_list.pdf"'
- # find the template and render it.
template = get_template(template_path)
html = template.render(context)
- # create a pdf
pisa_status = pisa.CreatePDF(html, dest=response)
- # if error then show some funny view
if pisa_status.err:
- return HttpResponse("We had some errors " + html + "") + return HttpResponse(f"We had some errors
{html}")
return response
@@ -490,53 +369,45 @@ def render_student_pdf_list(request):
@admin_required
def delete_student(request, pk):
student = get_object_or_404(Student, pk=pk)
- # full_name = student.user.get_full_name
+ full_name = student.student.get_full_name
student.delete()
- messages.success(request, "Student has been deleted.")
+ messages.success(request, f"Student {full_name} has been deleted.")
return redirect("student_list")
@login_required
@admin_required
def edit_student_program(request, pk):
-
- instance = get_object_or_404(Student, student_id=pk)
+ student = get_object_or_404(Student, student_id=pk)
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
- form = ProgramUpdateForm(request.POST, request.FILES, instance=instance)
- full_name = user.get_full_name
+ form = ProgramUpdateForm(request.POST, request.FILES, instance=student)
if form.is_valid():
form.save()
- messages.success(request, message=full_name + " program has been updated.")
- url = (
- "/accounts/profile/" + user.id.__str__() + "/detail/"
- ) # Botched job, must optimize
- return redirect(to=url)
- else:
- messages.error(request, "Please correct the error(s) below.")
+ full_name = user.get_full_name
+ messages.success(request, f"{full_name}'s program has been updated.")
+ return redirect("profile_single", user_id=pk)
+ messages.error(request, "Please correct the error(s) below.")
else:
- form = ProgramUpdateForm(instance=instance)
+ form = ProgramUpdateForm(instance=student)
return render(
request,
"accounts/edit_student_program.html",
- context={"title": "Edit-program", "form": form, "student": instance},
+ {"title": "Edit Program", "form": form, "student": student},
)
# ########################################################
+# Parent Views
+# ########################################################
+@method_decorator([login_required, admin_required], name="dispatch")
class ParentAdd(CreateView):
model = Parent
form_class = ParentAddForm
template_name = "accounts/parent_form.html"
-
-# def parent_add(request):
-# if request.method == 'POST':
-# form = ParentAddForm(request.POST)
-# if form.is_valid():
-# form.save()
-# return redirect('student_list')
-# else:
-# form = ParentAddForm(request.POST)
+ def form_valid(self, form):
+ messages.success(self.request, "Parent added successfully.")
+ return super().form_valid(form)
diff --git a/config/asgi.py b/config/asgi.py
index c6674cb..3d6b080 100644
--- a/config/asgi.py
+++ b/config/asgi.py
@@ -1,15 +1,6 @@
import os
-
-import django
-from channels.http import AsgiHandler
-from channels.routing import ProtocolTypeRouter
+from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
-django.setup()
-application = ProtocolTypeRouter(
- {
- "http": AsgiHandler(),
- # Just HTTP for now. (We can add other protocols later.)
- }
-)
+application = get_asgi_application()
diff --git a/config/settings.py b/config/settings.py
index 8d85727..cc45d3a 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -104,9 +104,6 @@ TEMPLATES = [
WSGI_APPLICATION = "config.wsgi.application"
-ASGI_APPLICATION = "config.asgi.application"
-
-
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
@@ -142,7 +139,10 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
-gettext = lambda s: s
+
+def gettext(s):
+ return s
+
LANGUAGES = (
("en", gettext("English")),
diff --git a/core/admin.py b/core/admin.py
index 5e47b18..6bec2bf 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,12 +1,12 @@
from django.contrib import admin
-from django.contrib.auth.models import Group
-
-from .models import Session, Semester, NewsAndEvents
from modeltranslation.admin import TranslationAdmin
+from .models import Session, Semester, NewsAndEvents
+
class NewsAndEventsAdmin(TranslationAdmin):
pass
+
admin.site.register(Semester)
admin.site.register(Session)
admin.site.register(NewsAndEvents, NewsAndEventsAdmin)
diff --git a/core/forms.py b/core/forms.py
index d30a557..55c1dff 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -1,6 +1,4 @@
from django import forms
-from django.db import transaction
-
from .models import NewsAndEvents, Session, Semester, SEMESTER
diff --git a/core/models.py b/core/models.py
index 9d228e0..b0881fd 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,7 +1,4 @@
from django.db import models
-from django.urls import reverse
-from django.core.validators import FileExtensionValidator
-from django.contrib.auth.models import AbstractUser
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -64,7 +61,7 @@ class NewsAndEvents(models.Model):
objects = NewsAndEventsManager()
def __str__(self):
- return self.title
+ return f"{self.title}"
class Session(models.Model):
@@ -73,7 +70,7 @@ class Session(models.Model):
next_session_begins = models.DateField(blank=True, null=True)
def __str__(self):
- return self.session
+ return f"{self.session}"
class Semester(models.Model):
@@ -85,7 +82,7 @@ class Semester(models.Model):
next_semester_begins = models.DateField(null=True, blank=True)
def __str__(self):
- return self.semester
+ return f"{self.semester}"
class ActivityLog(models.Model):
diff --git a/core/tests.py b/core/tests.py
index 7ce503c..a79ca8b 100644
--- a/core/tests.py
+++ b/core/tests.py
@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
# Create your tests here.
diff --git a/core/translation.py b/core/translation.py
index f0eb1f4..deeaeb7 100644
--- a/core/translation.py
+++ b/core/translation.py
@@ -1,8 +1,11 @@
from modeltranslation.translator import register, TranslationOptions
-from .models import NewsAndEvents, ActivityLog
+from .models import NewsAndEvents
+
@register(NewsAndEvents)
class NewsAndEventsTranslationOptions(TranslationOptions):
- fields = ('title', 'summary',)
- empty_values=None
-
+ fields = (
+ "title",
+ "summary",
+ )
+ empty_values = None
diff --git a/core/views.py b/core/views.py
index 5858148..c0d2cf1 100644
--- a/core/views.py
+++ b/core/views.py
@@ -41,24 +41,15 @@ def dashboard_view(request):
def post_add(request):
if request.method == "POST":
form = NewsAndEventsForm(request.POST)
- title = request.POST.get("title")
+ title = form.cleaned_data.get("title", "Post") if form.is_valid() else None
if form.is_valid():
form.save()
-
- messages.success(request, (title + " has been uploaded."))
+ messages.success(request, f"{title} has been uploaded.")
return redirect("home")
- else:
- messages.error(request, "Please correct the error(s) below.")
+ messages.error(request, "Please correct the error(s) below.")
else:
form = NewsAndEventsForm()
- return render(
- request,
- "core/post_add.html",
- {
- "title": "Add Post",
- "form": form,
- },
- )
+ return render(request, "core/post_add.html", {"title": "Add Post", "form": form})
@login_required
@@ -67,33 +58,24 @@ def edit_post(request, pk):
instance = get_object_or_404(NewsAndEvents, pk=pk)
if request.method == "POST":
form = NewsAndEventsForm(request.POST, instance=instance)
- title = request.POST.get("title")
+ title = form.cleaned_data.get("title", "Post") if form.is_valid() else None
if form.is_valid():
form.save()
-
- messages.success(request, (title + " has been updated."))
+ messages.success(request, f"{title} has been updated.")
return redirect("home")
- else:
- messages.error(request, "Please correct the error(s) below.")
+ messages.error(request, "Please correct the error(s) below.")
else:
form = NewsAndEventsForm(instance=instance)
- return render(
- request,
- "core/post_add.html",
- {
- "title": "Edit Post",
- "form": form,
- },
- )
+ return render(request, "core/post_add.html", {"title": "Edit Post", "form": form})
@login_required
@lecturer_required
def delete_post(request, pk):
post = get_object_or_404(NewsAndEvents, pk=pk)
- title = post.title
+ post_title = post.title
post.delete()
- messages.success(request, (title + " has been deleted."))
+ messages.success(request, f"{post_title} has been deleted.")
return redirect("home")
@@ -111,30 +93,15 @@ def session_list_view(request):
@login_required
@lecturer_required
def session_add_view(request):
- """check request method, if POST we add session otherwise show empty form"""
+ """Add a new session"""
if request.method == "POST":
form = SessionForm(request.POST)
if form.is_valid():
- data = form.data.get(
- "is_current_session"
- ) # returns string of 'True' if the user selected Yes
- print(data)
- if data == "true":
- sessions = Session.objects.all()
- if sessions:
- for session in sessions:
- if session.is_current_session == True:
- unset = Session.objects.get(is_current_session=True)
- unset.is_current_session = False
- unset.save()
- form.save()
- else:
- form.save()
- else:
- form.save()
- messages.success(request, "Session added successfully. ")
+ if form.cleaned_data.get("is_current_session"):
+ unset_current_session()
+ form.save()
+ messages.success(request, "Session added successfully.")
return redirect("session_list")
-
else:
form = SessionForm()
return render(request, "core/session_update.html", {"form": form})
@@ -143,30 +110,15 @@ def session_add_view(request):
@login_required
@lecturer_required
def session_update_view(request, pk):
- session = Session.objects.get(pk=pk)
+ session = get_object_or_404(Session, pk=pk)
if request.method == "POST":
form = SessionForm(request.POST, instance=session)
- data = form.data.get("is_current_session")
- if data == "true":
- sessions = Session.objects.all()
- if sessions:
- for session in sessions:
- if session.is_current_session == True:
- unset = Session.objects.get(is_current_session=True)
- unset.is_current_session = False
- unset.save()
-
- if form.is_valid():
- form.save()
- messages.success(request, "Session updated successfully. ")
- return redirect("session_list")
- else:
- form = SessionForm(request.POST, instance=session)
- if form.is_valid():
- form.save()
- messages.success(request, "Session updated successfully. ")
- return redirect("session_list")
-
+ if form.is_valid():
+ if form.cleaned_data.get("is_current_session"):
+ unset_current_session()
+ form.save()
+ messages.success(request, "Session updated successfully.")
+ return redirect("session_list")
else:
form = SessionForm(instance=session)
return render(request, "core/session_update.html", {"form": form})
@@ -176,17 +128,20 @@ def session_update_view(request, pk):
@lecturer_required
def session_delete_view(request, pk):
session = get_object_or_404(Session, pk=pk)
-
if session.is_current_session:
- messages.error(request, "You cannot delete current session")
- return redirect("session_list")
+ messages.error(request, "You cannot delete the current session.")
else:
session.delete()
- messages.success(request, "Session successfully deleted")
+ messages.success(request, "Session successfully deleted.")
return redirect("session_list")
-# ########################################################
+def unset_current_session():
+ """Unset current session"""
+ current_session = Session.objects.filter(is_current_session=True).first()
+ if current_session:
+ current_session.is_current_session = False
+ current_session.save()
# ########################################################
@@ -196,13 +151,7 @@ def session_delete_view(request, pk):
@lecturer_required
def semester_list_view(request):
semesters = Semester.objects.all().order_by("-is_current_semester", "-semester")
- return render(
- request,
- "core/semester_list.html",
- {
- "semesters": semesters,
- },
- )
+ return render(request, "core/semester_list.html", {"semesters": semesters})
@login_required
@@ -211,52 +160,11 @@ def semester_add_view(request):
if request.method == "POST":
form = SemesterForm(request.POST)
if form.is_valid():
- data = form.data.get(
- "is_current_semester"
- ) # returns string of 'True' if the user selected Yes
- if data == "True":
- semester = form.data.get("semester")
- ss = form.data.get("session")
- session = Session.objects.get(pk=ss)
- try:
- if Semester.objects.get(semester=semester, session=ss):
- messages.error(
- request,
- semester
- + " semester in "
- + session.session
- + " session already exist",
- )
- return redirect("add_semester")
- except:
- semesters = Semester.objects.all()
- sessions = Session.objects.all()
- if semesters:
- for semester in semesters:
- if semester.is_current_semester == True:
- unset_semester = Semester.objects.get(
- is_current_semester=True
- )
- unset_semester.is_current_semester = False
- unset_semester.save()
- for session in sessions:
- if session.is_current_session == True:
- unset_session = Session.objects.get(
- is_current_session=True
- )
- unset_session.is_current_session = False
- unset_session.save()
-
- new_session = request.POST.get("session")
- set_session = Session.objects.get(pk=new_session)
- set_session.is_current_session = True
- set_session.save()
- form.save()
- messages.success(request, "Semester added successfully.")
- return redirect("semester_list")
-
+ if form.cleaned_data.get("is_current_semester"):
+ unset_current_semester()
+ unset_current_session()
form.save()
- messages.success(request, "Semester added successfully. ")
+ messages.success(request, "Semester added successfully.")
return redirect("semester_list")
else:
form = SemesterForm()
@@ -266,32 +174,16 @@ def semester_add_view(request):
@login_required
@lecturer_required
def semester_update_view(request, pk):
- semester = Semester.objects.get(pk=pk)
+ semester = get_object_or_404(Semester, pk=pk)
if request.method == "POST":
- if (
- request.POST.get("is_current_semester") == "True"
- ): # returns string of 'True' if the user selected yes for 'is current semester'
- unset_semester = Semester.objects.get(is_current_semester=True)
- unset_semester.is_current_semester = False
- unset_semester.save()
- unset_session = Session.objects.get(is_current_session=True)
- unset_session.is_current_session = False
- unset_session.save()
- new_session = request.POST.get("session")
- form = SemesterForm(request.POST, instance=semester)
- if form.is_valid():
- set_session = Session.objects.get(pk=new_session)
- set_session.is_current_session = True
- set_session.save()
- form.save()
- messages.success(request, "Semester updated successfully !")
- return redirect("semester_list")
- else:
- form = SemesterForm(request.POST, instance=semester)
- if form.is_valid():
- form.save()
- return redirect("semester_list")
-
+ form = SemesterForm(request.POST, instance=semester)
+ if form.is_valid():
+ if form.cleaned_data.get("is_current_semester"):
+ unset_current_semester()
+ unset_current_session()
+ form.save()
+ messages.success(request, "Semester updated successfully!")
+ return redirect("semester_list")
else:
form = SemesterForm(instance=semester)
return render(request, "core/semester_update.html", {"form": form})
@@ -302,9 +194,16 @@ def semester_update_view(request, pk):
def semester_delete_view(request, pk):
semester = get_object_or_404(Semester, pk=pk)
if semester.is_current_semester:
- messages.error(request, "You cannot delete current semester")
- return redirect("semester_list")
+ messages.error(request, "You cannot delete the current semester.")
else:
semester.delete()
- messages.success(request, "Semester successfully deleted")
+ messages.success(request, "Semester successfully deleted.")
return redirect("semester_list")
+
+
+def unset_current_semester():
+ """Unset current semester"""
+ current_semester = Semester.objects.filter(is_current_semester=True).first()
+ if current_semester:
+ current_semester.is_current_semester = False
+ current_semester.save()
diff --git a/course/models.py b/course/models.py
index e530d35..89c900e 100644
--- a/course/models.py
+++ b/course/models.py
@@ -1,40 +1,39 @@
-from django.db import models
-from django.urls import reverse
from django.conf import settings
from django.core.validators import FileExtensionValidator
-from django.db.models.signals import pre_save, post_save, post_delete
+from django.db import models
from django.db.models import Q
+from django.db.models.signals import pre_save, post_delete, post_save
from django.dispatch import receiver
+from django.urls import reverse
from django.utils.translation import gettext_lazy as _
-# project import
-from .utils import *
from core.models import ActivityLog
+from core.models import Semester
+from .utils import unique_slug_generator
+# Constants
YEARS = (
(1, "1"),
(2, "2"),
(3, "3"),
(4, "4"),
- (4, "5"),
- (4, "6"),
+ (5, "5"),
+ (6, "6"),
)
-# LEVEL_COURSE = "Level course"
-BACHELOR_DEGREE = _("Bachelor")
-MASTER_DEGREE = _("Master")
+BACHELOR_DEGREE = "Bachelor"
+MASTER_DEGREE = "Master"
-LEVEL = (
- # (LEVEL_COURSE, "Level course"),
+LEVEL_CHOICES = (
(BACHELOR_DEGREE, _("Bachelor Degree")),
(MASTER_DEGREE, _("Master Degree")),
)
-FIRST = _("First")
-SECOND = _("Second")
-THIRD = _("Third")
+FIRST = "First"
+SECOND = "Second"
+THIRD = "Third"
-SEMESTER = (
+SEMESTER_CHOICES = (
(FIRST, _("First")),
(SECOND, _("Second")),
(THIRD, _("Third")),
@@ -44,102 +43,91 @@ SEMESTER = (
class ProgramManager(models.Manager):
def search(self, query=None):
queryset = self.get_queryset()
- if query is not None:
+ if query:
or_lookup = Q(title__icontains=query) | Q(summary__icontains=query)
- queryset = queryset.filter(
- or_lookup
- ).distinct() # distinct() is often necessary with Q lookups
+ queryset = queryset.filter(or_lookup).distinct()
return queryset
class Program(models.Model):
title = models.CharField(max_length=150, unique=True)
- summary = models.TextField(null=True, blank=True)
+ summary = models.TextField(blank=True)
objects = ProgramManager()
def __str__(self):
- return self.title
+ return f"{self.title}"
def get_absolute_url(self):
return reverse("program_detail", kwargs={"pk": self.pk})
@receiver(post_save, sender=Program)
-def log_save(sender, instance, created, **kwargs):
+def log_program_save(sender, instance, created, **kwargs):
verb = "created" if created else "updated"
ActivityLog.objects.create(message=_(f"The program '{instance}' has been {verb}."))
@receiver(post_delete, sender=Program)
-def log_delete(sender, instance, **kwargs):
+def log_program_delete(sender, instance, **kwargs):
ActivityLog.objects.create(message=_(f"The program '{instance}' has been deleted."))
class CourseManager(models.Manager):
def search(self, query=None):
queryset = self.get_queryset()
- if query is not None:
+ if query:
or_lookup = (
Q(title__icontains=query)
| Q(summary__icontains=query)
| Q(code__icontains=query)
| Q(slug__icontains=query)
)
- queryset = queryset.filter(
- or_lookup
- ).distinct() # distinct() is often necessary with Q lookups
+ queryset = queryset.filter(or_lookup).distinct()
return queryset
class Course(models.Model):
- slug = models.SlugField(blank=True, unique=True)
- title = models.CharField(max_length=200, null=True)
- code = models.CharField(max_length=200, unique=True, null=True)
- credit = models.IntegerField(null=True, default=0)
- summary = models.TextField(max_length=200, blank=True, null=True)
+ slug = models.SlugField(unique=True, blank=True)
+ title = models.CharField(max_length=200)
+ code = models.CharField(max_length=200, unique=True)
+ credit = models.IntegerField(default=0)
+ summary = models.TextField(max_length=200, blank=True)
program = models.ForeignKey(Program, on_delete=models.CASCADE)
- level = models.CharField(max_length=25, choices=LEVEL, null=True)
- year = models.IntegerField(choices=YEARS, default=0)
- semester = models.CharField(choices=SEMESTER, max_length=200)
- is_elective = models.BooleanField(default=False, blank=True, null=True)
+ level = models.CharField(max_length=25, choices=LEVEL_CHOICES)
+ year = models.IntegerField(choices=YEARS, default=1)
+ semester = models.CharField(choices=SEMESTER_CHOICES, max_length=200)
+ is_elective = models.BooleanField(default=False)
objects = CourseManager()
def __str__(self):
- return "{0} ({1})".format(self.title, self.code)
+ return f"{self.title} ({self.code})"
def get_absolute_url(self):
return reverse("course_detail", kwargs={"slug": self.slug})
@property
def is_current_semester(self):
- from core.models import Semester
- current_semester = Semester.objects.get(is_current_semester=True)
-
- if self.semester == current_semester.semester:
- return True
- else:
- return False
+ current_semester = Semester.objects.filter(is_current_semester=True).first()
+ return self.semester == current_semester.semester if current_semester else False
-def course_pre_save_receiver(sender, instance, *args, **kwargs):
+@receiver(pre_save, sender=Course)
+def course_pre_save_receiver(sender, instance, **kwargs):
if not instance.slug:
instance.slug = unique_slug_generator(instance)
-pre_save.connect(course_pre_save_receiver, sender=Course)
-
-
@receiver(post_save, sender=Course)
-def log_save(sender, instance, created, **kwargs):
+def log_course_save(sender, instance, created, **kwargs):
verb = "created" if created else "updated"
ActivityLog.objects.create(message=_(f"The course '{instance}' has been {verb}."))
@receiver(post_delete, sender=Course)
-def log_delete(sender, instance, **kwargs):
+def log_course_delete(sender, instance, **kwargs):
ActivityLog.objects.create(message=_(f"The course '{instance}' has been deleted."))
@@ -147,9 +135,9 @@ class CourseAllocation(models.Model):
lecturer = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
- related_name=_("allocated_lecturer"),
+ related_name="allocated_lecturer",
)
- courses = models.ManyToManyField(Course, related_name=_("allocated_course"))
+ courses = models.ManyToManyField(Course, related_name="allocated_course")
session = models.ForeignKey(
"core.Session", on_delete=models.CASCADE, blank=True, null=True
)
@@ -166,7 +154,9 @@ class Upload(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
file = models.FileField(
upload_to="course_files/",
- help_text="Valid Files: pdf, docx, doc, xls, xlsx, ppt, pptx, zip, rar, 7zip",
+ help_text=_(
+ "Valid Files: pdf, docx, doc, xls, xlsx, ppt, pptx, zip, rar, 7zip"
+ ),
validators=[
FileExtensionValidator(
[
@@ -184,16 +174,14 @@ class Upload(models.Model):
)
],
)
- updated_date = models.DateTimeField(auto_now=True, auto_now_add=False, null=True)
- upload_time = models.DateTimeField(auto_now=False, auto_now_add=True, null=True)
+ updated_date = models.DateTimeField(auto_now=True)
+ upload_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
- return str(self.file)[6:]
+ return f"{self.title}"
def get_extension_short(self):
- ext = str(self.file).split(".")
- ext = ext[len(ext) - 1]
-
+ ext = self.file.name.split(".")[-1].lower()
if ext in ("doc", "docx"):
return "word"
elif ext == "pdf":
@@ -204,30 +192,28 @@ class Upload(models.Model):
return "powerpoint"
elif ext in ("zip", "rar", "7zip"):
return "archive"
+ return "file"
def delete(self, *args, **kwargs):
- self.file.delete()
+ self.file.delete(save=False)
super().delete(*args, **kwargs)
@receiver(post_save, sender=Upload)
-def log_save(sender, instance, created, **kwargs):
+def log_upload_save(sender, instance, created, **kwargs):
if created:
- ActivityLog.objects.create(
- message=_(
- f"The file '{instance.title}' has been uploaded to the course '{instance.course}'."
- )
+ message = _(
+ f"The file '{instance.title}' has been uploaded to the course '{instance.course}'."
)
else:
- ActivityLog.objects.create(
- message=_(
- f"The file '{instance.title}' of the course '{instance.course}' has been updated."
- )
+ message = _(
+ f"The file '{instance.title}' of the course '{instance.course}' has been updated."
)
+ ActivityLog.objects.create(message=message)
@receiver(post_delete, sender=Upload)
-def log_delete(sender, instance, **kwargs):
+def log_upload_delete(sender, instance, **kwargs):
ActivityLog.objects.create(
message=_(
f"The file '{instance.title}' of the course '{instance.course}' has been deleted."
@@ -237,7 +223,7 @@ def log_delete(sender, instance, **kwargs):
class UploadVideo(models.Model):
title = models.CharField(max_length=100)
- slug = models.SlugField(blank=True, unique=True)
+ slug = models.SlugField(unique=True, blank=True)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
video = models.FileField(
upload_to="course_videos/",
@@ -246,11 +232,11 @@ class UploadVideo(models.Model):
FileExtensionValidator(["mp4", "mkv", "wmv", "3gp", "f4v", "avi", "mp3"])
],
)
- summary = models.TextField(null=True, blank=True)
- timestamp = models.DateTimeField(auto_now=False, auto_now_add=True, null=True)
+ summary = models.TextField(blank=True)
+ timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
- return str(self.title)
+ return f"{self.title}"
def get_absolute_url(self):
return reverse(
@@ -258,36 +244,31 @@ class UploadVideo(models.Model):
)
def delete(self, *args, **kwargs):
- self.video.delete()
+ self.video.delete(save=False)
super().delete(*args, **kwargs)
-def video_pre_save_receiver(sender, instance, *args, **kwargs):
+@receiver(pre_save, sender=UploadVideo)
+def video_pre_save_receiver(sender, instance, **kwargs):
if not instance.slug:
instance.slug = unique_slug_generator(instance)
-pre_save.connect(video_pre_save_receiver, sender=UploadVideo)
-
-
@receiver(post_save, sender=UploadVideo)
-def log_save(sender, instance, created, **kwargs):
+def log_uploadvideo_save(sender, instance, created, **kwargs):
if created:
- ActivityLog.objects.create(
- message=_(
- f"The video '{instance.title}' has been uploaded to the course {instance.course}."
- )
+ message = _(
+ f"The video '{instance.title}' has been uploaded to the course '{instance.course}'."
)
else:
- ActivityLog.objects.create(
- message=_(
- f"The video '{instance.title}' of the course '{instance.course}' has been updated."
- )
+ message = _(
+ f"The video '{instance.title}' of the course '{instance.course}' has been updated."
)
+ ActivityLog.objects.create(message=message)
@receiver(post_delete, sender=UploadVideo)
-def log_delete(sender, instance, **kwargs):
+def log_uploadvideo_delete(sender, instance, **kwargs):
ActivityLog.objects.create(
message=_(
f"The video '{instance.title}' of the course '{instance.course}' has been deleted."
@@ -296,9 +277,9 @@ def log_delete(sender, instance, **kwargs):
class CourseOffer(models.Model):
- _("""NOTE: Only department head can offer semester courses""")
+ """NOTE: Only department head can offer semester courses"""
dep_head = models.ForeignKey("accounts.DepartmentHead", on_delete=models.CASCADE)
def __str__(self):
- return "{}".format(self.dep_head)
+ return str(self.dep_head)
diff --git a/course/urls.py b/course/urls.py
index d2b1e0f..ef29b76 100644
--- a/course/urls.py
+++ b/course/urls.py
@@ -1,73 +1,77 @@
from django.urls import path
-from .views import *
+from . import views
urlpatterns = [
# Program urls
- path("", ProgramFilterView.as_view(), name="programs"),
- path("