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("/detail/", program_detail, name="program_detail"), - path("add/", program_add, name="add_program"), - path("/edit/", program_edit, name="edit_program"), - path("/delete/", program_delete, name="program_delete"), + path("", views.ProgramFilterView.as_view(), name="programs"), + path("/detail/", views.program_detail, name="program_detail"), + path("add/", views.program_add, name="add_program"), + path("/edit/", views.program_edit, name="edit_program"), + path("/delete/", views.program_delete, name="program_delete"), # Course urls - path("course//detail/", course_single, name="course_detail"), - path("/course/add/", course_add, name="course_add"), - path("course//edit/", course_edit, name="edit_course"), - path("course/delete//", course_delete, name="delete_course"), + path("course//detail/", views.course_single, name="course_detail"), + path("/course/add/", views.course_add, name="course_add"), + path("course//edit/", views.course_edit, name="edit_course"), + path("course/delete//", views.course_delete, name="delete_course"), # CourseAllocation urls path( - "course/assign/", CourseAllocationFormView.as_view(), name="course_allocation" + "course/assign/", + views.CourseAllocationFormView.as_view(), + name="course_allocation", ), path( "course/allocated/", - CourseAllocationFilterView.as_view(), + views.CourseAllocationFilterView.as_view(), name="course_allocation_view", ), path( "allocated_course//edit/", - edit_allocated_course, + views.edit_allocated_course, name="edit_allocated_course", ), - path("course//deallocate/", deallocate_course, name="course_deallocate"), + path( + "course//deallocate/", views.deallocate_course, name="course_deallocate" + ), # File uploads urls path( "course//documentations/upload/", - handle_file_upload, + views.handle_file_upload, name="upload_file_view", ), path( "course//documentations//edit/", - handle_file_edit, + views.handle_file_edit, name="upload_file_edit", ), path( "course//documentations//delete/", - handle_file_delete, + views.handle_file_delete, name="upload_file_delete", ), # Video uploads urls path( "course//video_tutorials/upload/", - handle_video_upload, + views.handle_video_upload, name="upload_video", ), path( "course//video_tutorials//detail/", - handle_video_single, + views.handle_video_single, name="video_single", ), path( "course//video_tutorials//edit/", - handle_video_edit, + views.handle_video_edit, name="upload_video_edit", ), path( "course//video_tutorials//delete/", - handle_video_delete, + views.handle_video_delete, name="upload_video_delete", ), # course registration - path("course/registration/", course_registration, name="course_registration"), - path("course/drop/", course_drop, name="course_drop"), - path("my_courses/", user_course_list, name="user_course_list"), + path("course/registration/", views.course_registration, name="course_registration"), + path("course/drop/", views.course_drop, name="course_drop"), + path("my_courses/", views.user_course_list, name="user_course_list"), ] diff --git a/course/views.py b/course/views.py index 1679294..a4b8080 100644 --- a/course/views.py +++ b/course/views.py @@ -1,28 +1,38 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.db.models import Sum, Avg, Max, Min, Count -from django.contrib.auth.decorators import login_required -from django.views.generic import CreateView -from django.core.paginator import Paginator from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.db.models import Sum +from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator -from django.views.generic import ListView +from django.views.generic import CreateView from django_filters.views import FilterView -from accounts.models import User, Student -from core.models import Session, Semester -from result.models import TakenCourse from accounts.decorators import lecturer_required, student_required -from .forms import ( - ProgramForm, +from accounts.models import Student +from core.models import Semester +from course.filters import CourseAllocationFilter, ProgramFilter +from course.forms import ( CourseAddForm, CourseAllocationForm, EditCourseAllocationForm, + ProgramForm, UploadFormFile, UploadFormVideo, ) -from .filters import ProgramFilter, CourseAllocationFilter -from .models import Program, Course, CourseAllocation, Upload, UploadVideo +from course.models import ( + Course, + CourseAllocation, + Program, + Upload, + UploadVideo, +) +from result.models import TakenCourse + + +# ######################################################## +# Program Views +# ######################################################## @method_decorator([login_required, lecturer_required], name="dispatch") @@ -42,37 +52,25 @@ def program_add(request): if request.method == "POST": form = ProgramForm(request.POST) if form.is_valid(): - form.save() - messages.success( - request, request.POST.get("title") + " program has been created." - ) + program = form.save() + messages.success(request, f"{program.title} program has been created.") return redirect("programs") - else: - messages.error(request, "Correct the error(S) below.") + messages.error(request, "Correct the error(s) below.") else: form = ProgramForm() - return render( - request, - "course/program_add.html", - { - "title": "Add Program", - "form": form, - }, + request, "course/program_add.html", {"title": "Add Program", "form": form} ) @login_required def program_detail(request, pk): - program = Program.objects.get(pk=pk) + program = get_object_or_404(Program, pk=pk) courses = Course.objects.filter(program_id=pk).order_by("-year") - credits = Course.objects.aggregate(Sum("credit")) - + credits = courses.aggregate(total_credits=Sum("credit")) paginator = Paginator(courses, 10) page = request.GET.get("page") - courses = paginator.get_page(page) - return render( request, "course/program_single.html", @@ -88,52 +86,42 @@ def program_detail(request, pk): @login_required @lecturer_required def program_edit(request, pk): - program = Program.objects.get(pk=pk) - + program = get_object_or_404(Program, pk=pk) if request.method == "POST": form = ProgramForm(request.POST, instance=program) if form.is_valid(): - form.save() - messages.success( - request, str(request.POST.get("title")) + " program has been updated." - ) + program = form.save() + messages.success(request, f"{program.title} program has been updated.") return redirect("programs") + messages.error(request, "Correct the error(s) below.") else: form = ProgramForm(instance=program) - return render( - request, - "course/program_add.html", - {"title": "Edit Program", "form": form}, + request, "course/program_add.html", {"title": "Edit Program", "form": form} ) @login_required @lecturer_required def program_delete(request, pk): - program = Program.objects.get(pk=pk) + program = get_object_or_404(Program, pk=pk) title = program.title program.delete() - messages.success(request, "Program " + title + " has been deleted.") - + messages.success(request, f"Program {title} has been deleted.") return redirect("programs") # ######################################################## +# Course Views +# ######################################################## -# ######################################################## -# Course views -# ######################################################## @login_required def course_single(request, slug): - course = Course.objects.get(slug=slug) + course = get_object_or_404(Course, slug=slug) files = Upload.objects.filter(course__slug=slug) videos = UploadVideo.objects.filter(course__slug=slug) - - # lecturers = User.objects.filter(allocated_lecturer__pk=course.id) lecturers = CourseAllocation.objects.filter(courses__pk=course.id) - return render( request, "course/course_single.html", @@ -143,7 +131,7 @@ def course_single(request, slug): "files": files, "videos": videos, "lecturers": lecturers, - "media_url": settings.MEDIA_ROOT, + "media_url": settings.MEDIA_URL, }, ) @@ -151,31 +139,22 @@ def course_single(request, slug): @login_required @lecturer_required def course_add(request, pk): - users = User.objects.all() + program = get_object_or_404(Program, pk=pk) if request.method == "POST": form = CourseAddForm(request.POST) - course_name = request.POST.get("title") - course_code = request.POST.get("code") if form.is_valid(): - form.save() + course = form.save() messages.success( - request, (course_name + "(" + course_code + ")" + " has been created.") + request, f"{course.title} ({course.code}) has been created." ) - return redirect("program_detail", pk=request.POST.get("program")) - else: - messages.error(request, "Correct the error(s) below.") + return redirect("program_detail", pk=program.pk) + messages.error(request, "Correct the error(s) below.") else: - form = CourseAddForm(initial={"program": Program.objects.get(pk=pk)}) - + form = CourseAddForm(initial={"program": program}) return render( request, "course/course_add.html", - { - "title": "Add Course", - "form": form, - "program": pk, - "users": users, - }, + {"title": "Add Course", "form": form, "program": program}, ) @@ -185,73 +164,49 @@ def course_edit(request, slug): course = get_object_or_404(Course, slug=slug) if request.method == "POST": form = CourseAddForm(request.POST, instance=course) - course_name = request.POST.get("title") - course_code = request.POST.get("code") if form.is_valid(): - form.save() + course = form.save() messages.success( - request, (course_name + "(" + course_code + ")" + " has been updated.") + request, f"{course.title} ({course.code}) has been updated." ) - return redirect("program_detail", pk=request.POST.get("program")) - else: - messages.error(request, "Correct the error(s) below.") + return redirect("program_detail", pk=course.program.pk) + messages.error(request, "Correct the error(s) below.") else: form = CourseAddForm(instance=course) - return render( - request, - "course/course_add.html", - { - "title": "Edit Course", - # 'form': form, 'program': pk, 'course': pk - "form": form, - }, + request, "course/course_add.html", {"title": "Edit Course", "form": form} ) @login_required @lecturer_required def course_delete(request, slug): - course = Course.objects.get(slug=slug) - # course_name = course.title + course = get_object_or_404(Course, slug=slug) + title = course.title + program_id = course.program.id course.delete() - messages.success(request, "Course " + course.title + " has been deleted.") - - return redirect("program_detail", pk=course.program.id) + messages.success(request, f"Course {title} has been deleted.") + return redirect("program_detail", pk=program_id) # ######################################################## +# Course Allocation Views +# ######################################################## -# ######################################################## -# Course Allocation -# ######################################################## -@method_decorator([login_required], name="dispatch") +@method_decorator([login_required, lecturer_required], name="dispatch") class CourseAllocationFormView(CreateView): form_class = CourseAllocationForm template_name = "course/course_allocation_form.html" - def get_form_kwargs(self): - kwargs = super(CourseAllocationFormView, self).get_form_kwargs() - kwargs["user"] = self.request.user - return kwargs - def form_valid(self, form): - # if a staff has been allocated a course before update it else create new lecturer = form.cleaned_data["lecturer"] selected_courses = form.cleaned_data["courses"] - courses = () - for course in selected_courses: - courses += (course.pk,) - # print(courses) - - try: - a = CourseAllocation.objects.get(lecturer=lecturer) - except: - a = CourseAllocation.objects.create(lecturer=lecturer) - for i in range(0, selected_courses.count()): - a.courses.add(courses[i]) - a.save() + allocation, created = CourseAllocation.objects.get_or_create(lecturer=lecturer) + allocation.courses.set(selected_courses) + messages.success( + self.request, f"Courses allocated to {lecturer.get_full_name} successfully." + ) return redirect("course_allocation_view") def get_context_data(self, **kwargs): @@ -260,7 +215,7 @@ class CourseAllocationFormView(CreateView): return context -@method_decorator([login_required], name="dispatch") +@method_decorator([login_required, lecturer_required], name="dispatch") class CourseAllocationFilterView(FilterView): filterset_class = CourseAllocationFilter template_name = "course/course_allocation_view.html" @@ -274,53 +229,50 @@ class CourseAllocationFilterView(FilterView): @login_required @lecturer_required def edit_allocated_course(request, pk): - allocated = get_object_or_404(CourseAllocation, pk=pk) + allocation = get_object_or_404(CourseAllocation, pk=pk) if request.method == "POST": - form = EditCourseAllocationForm(request.POST, instance=allocated) + form = EditCourseAllocationForm(request.POST, instance=allocation) if form.is_valid(): form.save() - messages.success(request, "course assigned has been updated.") + messages.success(request, "Course allocation has been updated.") return redirect("course_allocation_view") + messages.error(request, "Correct the error(s) below.") else: - form = EditCourseAllocationForm(instance=allocated) - + form = EditCourseAllocationForm(instance=allocation) return render( request, "course/course_allocation_form.html", - {"title": "Edit Course Allocated", "form": form, "allocated": pk}, + {"title": "Edit Course Allocation", "form": form}, ) @login_required @lecturer_required def deallocate_course(request, pk): - course = CourseAllocation.objects.get(pk=pk) - course.delete() - messages.success(request, "successfully deallocate!") + allocation = get_object_or_404(CourseAllocation, pk=pk) + allocation.delete() + messages.success(request, "Successfully deallocated courses.") return redirect("course_allocation_view") # ######################################################## +# File Upload Views +# ######################################################## -# ######################################################## -# File Upload views -# ######################################################## @login_required @lecturer_required def handle_file_upload(request, slug): - course = Course.objects.get(slug=slug) + course = get_object_or_404(Course, slug=slug) if request.method == "POST": form = UploadFormFile(request.POST, request.FILES) if form.is_valid(): - obj = form.save(commit=False) - obj.course = course - obj.save() - - messages.success( - request, (request.POST.get("title") + " has been uploaded.") - ) + upload = form.save(commit=False) + upload.course = course + upload.save() + messages.success(request, f"{upload.title} has been uploaded.") return redirect("course_detail", slug=slug) + messages.error(request, "Correct the error(s) below.") else: form = UploadFormFile() return render( @@ -333,54 +285,52 @@ def handle_file_upload(request, slug): @login_required @lecturer_required def handle_file_edit(request, slug, file_id): - course = Course.objects.get(slug=slug) - instance = Upload.objects.get(pk=file_id) + course = get_object_or_404(Course, slug=slug) + upload = get_object_or_404(Upload, pk=file_id) if request.method == "POST": - form = UploadFormFile(request.POST, request.FILES, instance=instance) - # file_name = request.POST.get('name') + form = UploadFormFile(request.POST, request.FILES, instance=upload) if form.is_valid(): - form.save() - messages.success( - request, (request.POST.get("title") + " has been updated.") - ) + upload = form.save() + messages.success(request, f"{upload.title} has been updated.") return redirect("course_detail", slug=slug) + messages.error(request, "Correct the error(s) below.") else: - form = UploadFormFile(instance=instance) - + form = UploadFormFile(instance=upload) return render( request, "upload/upload_file_form.html", - {"title": instance.title, "form": form, "course": course}, + {"title": "Edit File", "form": form, "course": course}, ) +@login_required +@lecturer_required def handle_file_delete(request, slug, file_id): - file = Upload.objects.get(pk=file_id) - # file_name = file.name - file.delete() - - messages.success(request, (file.title + " has been deleted.")) + upload = get_object_or_404(Upload, pk=file_id) + title = upload.title + upload.delete() + messages.success(request, f"{title} has been deleted.") return redirect("course_detail", slug=slug) # ######################################################## -# Video Upload views +# Video Upload Views # ######################################################## + + @login_required @lecturer_required def handle_video_upload(request, slug): - course = Course.objects.get(slug=slug) + course = get_object_or_404(Course, slug=slug) if request.method == "POST": form = UploadFormVideo(request.POST, request.FILES) if form.is_valid(): - obj = form.save(commit=False) - obj.course = course - obj.save() - - messages.success( - request, (request.POST.get("title") + " has been uploaded.") - ) + video = form.save(commit=False) + video.course = course + video.save() + messages.success(request, f"{video.title} has been uploaded.") return redirect("course_detail", slug=slug) + messages.error(request, "Correct the error(s) below.") else: form = UploadFormVideo() return render( @@ -391,172 +341,123 @@ def handle_video_upload(request, slug): @login_required -# @lecturer_required def handle_video_single(request, slug, video_slug): course = get_object_or_404(Course, slug=slug) video = get_object_or_404(UploadVideo, slug=video_slug) - return render(request, "upload/video_single.html", {"video": video}) + return render( + request, + "upload/video_single.html", + {"video": video, "course": course}, + ) @login_required @lecturer_required def handle_video_edit(request, slug, video_slug): - course = Course.objects.get(slug=slug) - instance = UploadVideo.objects.get(slug=video_slug) + course = get_object_or_404(Course, slug=slug) + video = get_object_or_404(UploadVideo, slug=video_slug) if request.method == "POST": - form = UploadFormVideo(request.POST, request.FILES, instance=instance) + form = UploadFormVideo(request.POST, request.FILES, instance=video) if form.is_valid(): - form.save() - messages.success( - request, (request.POST.get("title") + " has been updated.") - ) + video = form.save() + messages.success(request, f"{video.title} has been updated.") return redirect("course_detail", slug=slug) + messages.error(request, "Correct the error(s) below.") else: - form = UploadFormVideo(instance=instance) - + form = UploadFormVideo(instance=video) return render( request, "upload/upload_video_form.html", - {"title": instance.title, "form": form, "course": course}, + {"title": "Edit Video", "form": form, "course": course}, ) +@login_required +@lecturer_required def handle_video_delete(request, slug, video_slug): video = get_object_or_404(UploadVideo, slug=video_slug) - # video = UploadVideo.objects.get(slug=video_slug) + title = video.title video.delete() - - messages.success(request, (video.title + " has been deleted.")) + messages.success(request, f"{title} has been deleted.") return redirect("course_detail", slug=slug) # ######################################################## +# Course Registration Views +# ######################################################## -# ######################################################## -# Course Registration -# ######################################################## @login_required @student_required def course_registration(request): + student = get_object_or_404(Student, student__pk=request.user.id) + current_semester = Semester.objects.filter(is_current_semester=True).first() + if not current_semester: + messages.error(request, "No active semester found.") + return render(request, "course/course_registration.html") + if request.method == "POST": - student = Student.objects.get(student__pk=request.user.id) - ids = () - data = request.POST.copy() - data.pop("csrfmiddlewaretoken", None) # remove csrf_token - for key in data.keys(): - ids = ids + (str(key),) - for s in range(0, len(ids)): - course = Course.objects.get(pk=ids[s]) - obj = TakenCourse.objects.create(student=student, course=course) - obj.save() + course_ids = request.POST.getlist("course_ids") + for course_id in course_ids: + course = get_object_or_404(Course, pk=course_id) + TakenCourse.objects.get_or_create(student=student, course=course) messages.success(request, "Courses registered successfully!") return redirect("course_registration") - else: - current_semester = Semester.objects.filter(is_current_semester=True).first() - if not current_semester: - messages.error(request, "No active semester found.") - return render(request, "course/course_registration.html") - # student = Student.objects.get(student__pk=request.user.id) - student = get_object_or_404(Student, student__id=request.user.id) - taken_courses = TakenCourse.objects.filter(student__student__id=request.user.id) - t = () - for i in taken_courses: - t += (i.course.pk,) + taken_course_ids = TakenCourse.objects.filter(student=student).values_list( + "course__id", flat=True + ) + courses = Course.objects.filter( + program=student.program, + level=student.level, + semester=current_semester.semester, + ).exclude(id__in=taken_course_ids) - courses = ( - Course.objects.filter( - program__pk=student.program.id, - level=student.level, - semester=current_semester, - ) - .exclude(id__in=t) - .order_by("year") - ) - all_courses = Course.objects.filter( - level=student.level, program__pk=student.program.id - ) + registered_courses = Course.objects.filter(id__in=taken_course_ids) + all_courses = Course.objects.filter(level=student.level, program=student.program) - no_course_is_registered = False # Check if no course is registered - all_courses_are_registered = False - - registered_courses = Course.objects.filter(level=student.level).filter(id__in=t) - if ( - registered_courses.count() == 0 - ): # Check if number of registered courses is 0 - no_course_is_registered = True - - if registered_courses.count() == all_courses.count(): - all_courses_are_registered = True - - total_first_semester_credit = 0 - total_sec_semester_credit = 0 - total_registered_credit = 0 - for i in courses: - if i.semester == "First": - total_first_semester_credit += int(i.credit) - if i.semester == "Second": - total_sec_semester_credit += int(i.credit) - for i in registered_courses: - total_registered_credit += int(i.credit) - context = { - "is_calender_on": True, - "all_courses_are_registered": all_courses_are_registered, - "no_course_is_registered": no_course_is_registered, - "current_semester": current_semester, - "courses": courses, - "total_first_semester_credit": total_first_semester_credit, - "total_sec_semester_credit": total_sec_semester_credit, - "registered_courses": registered_courses, - "total_registered_credit": total_registered_credit, - "student": student, - } - return render(request, "course/course_registration.html", context) + context = { + "courses": courses, + "registered_courses": registered_courses, + "student": student, + "current_semester": current_semester, + "all_courses_registered": all_courses.count() == registered_courses.count(), + } + return render(request, "course/course_registration.html", context) @login_required @student_required def course_drop(request): if request.method == "POST": - student = Student.objects.get(student__pk=request.user.id) - ids = () - data = request.POST.copy() - data.pop("csrfmiddlewaretoken", None) # remove csrf_token - for key in data.keys(): - ids = ids + (str(key),) - for s in range(0, len(ids)): - course = Course.objects.get(pk=ids[s]) - obj = TakenCourse.objects.get(student=student, course=course) - obj.delete() - messages.success(request, "Successfully Dropped!") + student = get_object_or_404(Student, student__pk=request.user.id) + course_ids = request.POST.getlist("course_ids") + for course_id in course_ids: + course = get_object_or_404(Course, pk=course_id) + TakenCourse.objects.filter(student=student, course=course).delete() + messages.success(request, "Courses dropped successfully!") return redirect("course_registration") # ######################################################## +# User Course List View +# ######################################################## @login_required def user_course_list(request): if request.user.is_lecturer: courses = Course.objects.filter(allocated_course__lecturer__pk=request.user.id) - return render(request, "course/user_course_list.html", {"courses": courses}) - elif request.user.is_student: - student = Student.objects.get(student__pk=request.user.id) - taken_courses = TakenCourse.objects.filter( - student__student__id=student.student.id - ) - courses = Course.objects.filter(level=student.level).filter( - program__pk=student.program.id - ) - + if request.user.is_student: + student = get_object_or_404(Student, student__pk=request.user.id) + taken_courses = TakenCourse.objects.filter(student=student) return render( request, "course/user_course_list.html", - {"student": student, "taken_courses": taken_courses, "courses": courses}, + {"student": student, "taken_courses": taken_courses}, ) - else: - return render(request, "course/user_course_list.html") + # For other users + return render(request, "course/user_course_list.html")