From 36a62591891690b6c2bf24c3a5bbe9222199d001 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 00:55:24 +0300 Subject: [PATCH 01/17] Following pylint rules --- .pylintrc | 663 ++++++++++++++++++++++++++++++ accounts/apps.py | 7 +- accounts/decorators.py | 17 +- accounts/signals.py | 2 +- accounts/tests.py | 17 +- accounts/tests/test_decorators.py | 53 ++- accounts/tests/test_filters.py | 96 ++++- accounts/utils.py | 2 +- accounts/views.py | 475 ++++++++------------- config/asgi.py | 13 +- config/settings.py | 8 +- core/admin.py | 6 +- core/forms.py | 2 - core/models.py | 9 +- core/tests.py | 2 +- core/translation.py | 11 +- core/views.py | 209 +++------- course/models.py | 165 ++++---- course/urls.py | 52 +-- course/views.py | 435 ++++++++------------ 20 files changed, 1308 insertions(+), 936 deletions(-) create mode 100644 .pylintrc 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 wrapper(request, *args, **kwargs): 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 wrapper(request, *args, **kwargs): 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 wrapper(request, *args, **kwargs): 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 @@ ) -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 @@ 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 @@ 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 @@ def test_admin_redirect_decorator_return_correct_response(self): 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 @@ def test_lecturer_required_decorator_redirects(self): 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 @@ def test_lecturer_required_decorator_redirects_to_correct_path(self): 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 @@ def test_lecturer_redirect_decorator_return_correct_response(self): 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 @@ def test_student_required_decorator_redirects(self): 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 @@ def test_student_required_decorator_redirects_to_correct_path(self): 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 @@ def test_student_redirect_decorator_return_correct_response(self): 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 @@ def test_name_filter(self): 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 ( - StaffAddForm, - StudentAddForm, - ProfileUpdateForm, +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, ) -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" + context["courses"] = courses + return render(request, "accounts/profile.html", context) + + 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=level.level + student__student__id=request.user.id, course__level=student.level ) - context = { - "title": request.user.get_full_name, - "parent": parent, - "courses": courses, - "level": level, - "current_session": current_session, - "current_semester": current_semester, - } - return render(request, "accounts/profile.html", context) - else: - staff = User.objects.filter(is_lecturer=True) - return render( - request, - "accounts/profile.html", + context.update( { - "title": request.user.get_full_name, - "staff": staff, - "current_session": current_session, - "current_semester": current_semester, - }, + "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 @@ def get_context_data(self, **kwargs): 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 @@ def get_context_data(self, **kwargs): 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 @@ 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 @@ # 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 @@ 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) + current_semester = Semester.objects.filter(is_current_semester=True).first() + return self.semester == current_semester.semester if current_semester else False - if self.semester == current_semester.semester: - return True - else: - return 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 @@ def get_extension_short(self): 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 @@ def get_absolute_url(self): ) 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.conf import settings 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.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 @@ def get_context_data(self, **kwargs): 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 @@ def get_context_data(self, **kwargs): @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,) - - 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 - ) - 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) + 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) + + registered_courses = Course.objects.filter(id__in=taken_course_ids) + all_courses = Course.objects.filter(level=student.level, program=student.program) + + 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") From cf81b3e8a329e069f5529390f4ac0f5807931b28 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 01:17:28 +0300 Subject: [PATCH 02/17] Follow pylint rule for quiz app --- .pylintrc | 3 +- quiz/models.py | 277 +++++++++++++++--------------------------- quiz/urls.py | 20 +-- quiz/views.py | 323 ++++++++++++++++++++++--------------------------- 4 files changed, 251 insertions(+), 372 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7f22e63..99d3a19 100644 --- a/.pylintrc +++ b/.pylintrc @@ -450,7 +450,8 @@ disable=raw-checker-failed, too-few-public-methods, arguments-differ, invalid-overridden-method, - unsupported-binary-operation + unsupported-binary-operation, + attribute-defined-outside-init # 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 diff --git a/quiz/models.py b/quiz/models.py index 0423d17..2600b94 100644 --- a/quiz/models.py +++ b/quiz/models.py @@ -1,23 +1,22 @@ -import re import json +import re -from django.db import models -from django.urls import reverse -from django.core.exceptions import ValidationError, ImproperlyConfigured +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.validators import ( MaxValueValidator, validate_comma_separated_integer_list, ) -from django.utils.translation import gettext_lazy as _ -from django.utils.timezone import now -from django.conf import settings -from django.db.models.signals import pre_save - +from django.db import models from django.db.models import Q - +from django.db.models.signals import pre_save +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ from model_utils.managers import InheritanceManager + from course.models import Course -from .utils import * +from .utils import unique_slug_generator CHOICE_ORDER_OPTIONS = ( ("content", _("Content")), @@ -34,97 +33,69 @@ class QuizManager(models.Manager): def search(self, query=None): - qs = self.get_queryset() - if query is not None: + queryset = self.get_queryset() + if query: or_lookup = ( Q(title__icontains=query) | Q(description__icontains=query) | Q(category__icontains=query) | Q(slug__icontains=query) ) - qs = qs.filter( - or_lookup - ).distinct() # distinct() is often necessary with Q lookups - return qs + queryset = queryset.filter(or_lookup).distinct() + return queryset class Quiz(models.Model): - course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True) - title = models.CharField(verbose_name=_("Title"), max_length=60, blank=False) - slug = models.SlugField(blank=True, unique=True) + course = models.ForeignKey(Course, on_delete=models.CASCADE) + title = models.CharField(verbose_name=_("Title"), max_length=60) + slug = models.SlugField(unique=True, blank=True) description = models.TextField( verbose_name=_("Description"), blank=True, help_text=_("A detailed description of the quiz"), ) - category = models.TextField(choices=CATEGORY_OPTIONS, blank=True) + category = models.CharField(max_length=20, choices=CATEGORY_OPTIONS, blank=True) random_order = models.BooleanField( - blank=False, default=False, verbose_name=_("Random Order"), help_text=_("Display the questions in a random order or as they are set?"), ) - - # max_questions = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Max Questions"), - # help_text=_("Number of questions to be answered on each attempt.")) - answers_at_end = models.BooleanField( - blank=False, default=False, verbose_name=_("Answers at end"), help_text=_( "Correct answer is NOT shown after question. Answers displayed at the end." ), ) - exam_paper = models.BooleanField( - blank=False, default=False, verbose_name=_("Exam Paper"), help_text=_( "If yes, the result of each attempt by a user will be stored. Necessary for marking." ), ) - single_attempt = models.BooleanField( - blank=False, default=False, verbose_name=_("Single Attempt"), help_text=_("If yes, only one attempt by a user will be permitted."), ) - pass_mark = models.SmallIntegerField( - blank=True, default=50, verbose_name=_("Pass Mark"), validators=[MaxValueValidator(100)], help_text=_("Percentage required to pass exam."), ) - draft = models.BooleanField( - blank=True, default=False, verbose_name=_("Draft"), help_text=_( "If yes, the quiz is not displayed in the quiz list and can only be taken by users who can edit quizzes." ), ) - timestamp = models.DateTimeField(auto_now=True) objects = QuizManager() - def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.single_attempt is True: - self.exam_paper = True - - if self.pass_mark > 100: - raise ValidationError("%s is above 100" % self.pass_mark) - if self.pass_mark < 0: - raise ValidationError("%s is below 0" % self.pass_mark) - - super(Quiz, self).save(force_insert, force_update, *args, **kwargs) - class Meta: verbose_name = _("Quiz") verbose_name_plural = _("Quizzes") @@ -132,6 +103,15 @@ class Meta: def __str__(self): return self.title + def save(self, *args, **kwargs): + if self.single_attempt: + self.exam_paper = True + + if not (0 <= self.pass_mark <= 100): + raise ValidationError(_("Pass mark must be between 0 and 100.")) + + super().save(*args, **kwargs) + def get_questions(self): return self.question_set.all().select_subclasses() @@ -140,22 +120,18 @@ def get_max_score(self): return self.get_questions().count() def get_absolute_url(self): - # return reverse('quiz_start_page', kwargs={'pk': self.pk}) return reverse("quiz_index", kwargs={"slug": self.course.slug}) -def quiz_pre_save_receiver(sender, instance, *args, **kwargs): +@receiver(pre_save, sender=Quiz) +def quiz_pre_save_receiver(sender, instance, **kwargs): if not instance.slug: instance.slug = unique_slug_generator(instance) -pre_save.connect(quiz_pre_save_receiver, sender=Quiz) - - class ProgressManager(models.Manager): def new_progress(self, user): new_progress = self.create(user=user, score="") - new_progress.save() return new_progress @@ -175,51 +151,25 @@ class Meta: verbose_name = _("User Progress") verbose_name_plural = _("User progress records") - # @property def list_all_cat_scores(self): - score_before = self.score - output = {} - - if len(self.score) > len(score_before): - # If a new category has been added, save changes. - self.save() - - return output + return {} # Implement as needed def update_score(self, question, score_to_add=0, possible_to_add=0): - # category_test = Category.objects.filter(category=question.category).exists() - - if any( - [ - item is False - for item in [ - score_to_add, - possible_to_add, - isinstance(score_to_add, int), - isinstance(possible_to_add, int), - ] - ] - ): - return _("error"), _("category does not exist or invalid score") + if not isinstance(score_to_add, int) or not isinstance(possible_to_add, int): + return _("Error"), _("Invalid score values.") to_find = re.escape(str(question.quiz)) + r",(?P\d+),(?P\d+)," - match = re.search(to_find, self.score, re.IGNORECASE) if match: updated_score = int(match.group("score")) + abs(score_to_add) updated_possible = int(match.group("possible")) + abs(possible_to_add) - new_score = ",".join( [str(question.quiz), str(updated_score), str(updated_possible), ""] ) - - # swap old score for the new one self.score = self.score.replace(match.group(), new_score) self.save() - else: - # if not present but existing, add with the points passed in self.score += ",".join( [str(question.quiz), str(score_to_add), str(possible_to_add), ""] ) @@ -236,22 +186,20 @@ def show_exams(self): class SittingManager(models.Manager): def new_sitting(self, user, quiz, course): - if quiz.random_order is True: + if quiz.random_order: question_set = quiz.question_set.all().select_subclasses().order_by("?") else: question_set = quiz.question_set.all().select_subclasses() - question_set = [item.id for item in question_set] - - if len(question_set) == 0: + question_ids = [item.id for item in question_set] + if not question_ids: raise ImproperlyConfigured( - _("Question set of the quiz is empty. Please configure questions properly") + _( + "Question set of the quiz is empty. Please configure questions properly." + ) ) - # if quiz.max_questions and quiz.max_questions < len(question_set): - # question_set = question_set[:quiz.max_questions] - - questions = ",".join(map(str, question_set)) + "," + questions = ",".join(map(str, question_ids)) + "," new_sitting = self.create( user=user, @@ -268,7 +216,7 @@ def new_sitting(self, user, quiz, course): def user_sitting(self, user, quiz, course): if ( - quiz.single_attempt is True + quiz.single_attempt and self.filter(user=user, quiz=quiz, course=course, complete=True).exists() ): return False @@ -277,9 +225,9 @@ def user_sitting(self, user, quiz, course): except Sitting.DoesNotExist: sitting = self.new_sitting(user, quiz, course) except Sitting.MultipleObjectsReturned: - sitting = self.filter(user=user, quiz=quiz, course=course, complete=False)[ - 0 - ] + sitting = self.filter( + user=user, quiz=quiz, course=course, complete=False + ).first() return sitting @@ -289,32 +237,26 @@ class Sitting(models.Model): ) quiz = models.ForeignKey(Quiz, verbose_name=_("Quiz"), on_delete=models.CASCADE) course = models.ForeignKey( - Course, null=True, verbose_name=_("Course"), on_delete=models.CASCADE + Course, verbose_name=_("Course"), on_delete=models.CASCADE ) - question_order = models.CharField( max_length=1024, verbose_name=_("Question Order"), validators=[validate_comma_separated_integer_list], ) - question_list = models.CharField( max_length=1024, verbose_name=_("Question List"), validators=[validate_comma_separated_integer_list], ) - incorrect_questions = models.CharField( max_length=1024, blank=True, verbose_name=_("Incorrect questions"), validators=[validate_comma_separated_integer_list], ) - current_score = models.IntegerField(verbose_name=_("Current Score")) - complete = models.BooleanField( - default=False, blank=False, verbose_name=_("Complete") - ) + complete = models.BooleanField(default=False, verbose_name=_("Complete")) user_answers = models.TextField( blank=True, default="{}", verbose_name=_("User Answers") ) @@ -329,17 +271,14 @@ class Meta: def get_first_question(self): if not self.question_list: return False - - first, _ = self.question_list.split(",", 1) - question_id = int(first) - return Question.objects.get_subclass(id=question_id) + first_question_id = int(self.question_list.split(",", 1)[0]) + return Question.objects.get_subclass(id=first_question_id) def remove_first_question(self): if not self.question_list: return - - _, others = self.question_list.split(",", 1) - self.question_list = others + _, remaining_questions = self.question_list.split(",", 1) + self.question_list = remaining_questions self.save() def add_to_score(self, points): @@ -351,24 +290,15 @@ def get_current_score(self): return self.current_score def _question_ids(self): - return [int(n) for n in self.question_order.split(",") if n] + return [int(q) for q in self.question_order.split(",") if q] @property def get_percent_correct(self): - dividend = float(self.current_score) - divisor = len(self._question_ids()) - if divisor < 1: - return 0 # prevent divide by zero error - - if dividend > divisor: - return 100 - - correct = int(round((dividend / divisor) * 100)) - - if correct >= 1: - return correct - else: + total_questions = len(self._question_ids()) + if total_questions == 0: return 0 + percent = (self.current_score / total_questions) * 100 + return min(max(int(round(percent)), 0), 100) def mark_quiz_complete(self): self.complete = True @@ -376,9 +306,9 @@ def mark_quiz_complete(self): self.save() def add_incorrect_question(self, question): - if len(self.incorrect_questions) > 0: - self.incorrect_questions += "," - self.incorrect_questions += str(question.id) + "," + incorrect_ids = self.get_incorrect_questions + incorrect_ids.append(question.id) + self.incorrect_questions = ",".join(map(str, incorrect_ids)) + "," if self.complete: self.add_to_score(-1) self.save() @@ -388,11 +318,12 @@ def get_incorrect_questions(self): return [int(q) for q in self.incorrect_questions.split(",") if q] def remove_incorrect_question(self, question): - current = self.get_incorrect_questions - current.remove(question.id) - self.incorrect_questions = ",".join(map(str, current)) - self.add_to_score(1) - self.save() + incorrect_ids = self.get_incorrect_questions + if question.id in incorrect_ids: + incorrect_ids.remove(question.id) + self.incorrect_questions = ",".join(map(str, incorrect_ids)) + "," + self.add_to_score(1) + self.save() @property def check_if_passed(self): @@ -401,14 +332,14 @@ def check_if_passed(self): @property def result_message(self): if self.check_if_passed: - return _(f"You have passed this quiz, congratulation") + return _("You have passed this quiz, congratulations!") else: - return _(f"You failed this quiz, give it one chance again.") + return _("You failed this quiz, try again.") def add_user_answer(self, question, guess): - current = json.loads(self.user_answers) - current[question.id] = guess - self.user_answers = json.dumps(current) + user_answers = json.loads(self.user_answers) + user_answers[str(question.id)] = guess + self.user_answers = json.dumps(user_answers) self.save() def get_questions(self, with_answers=False): @@ -417,12 +348,10 @@ def get_questions(self, with_answers=False): self.quiz.question_set.filter(id__in=question_ids).select_subclasses(), key=lambda q: question_ids.index(q.id), ) - if with_answers: user_answers = json.loads(self.user_answers) for question in questions: - question.user_answer = user_answers[str(question.id)] - + question.user_answer = user_answers.get(str(question.id)) return questions @property @@ -444,13 +373,11 @@ class Question(models.Model): figure = models.ImageField( upload_to="uploads/%Y/%m/%d", blank=True, - null=True, verbose_name=_("Figure"), - help_text=_("Add an image for the question if it's necessary."), + help_text=_("Add an image for the question if necessary."), ) content = models.CharField( max_length=1000, - blank=False, help_text=_("Enter the question text that you want displayed"), verbose_name=_("Question"), ) @@ -474,79 +401,76 @@ def __str__(self): class MCQuestion(Question): choice_order = models.CharField( max_length=30, - null=True, - blank=True, choices=CHOICE_ORDER_OPTIONS, + blank=True, help_text=_( - "The order in which multichoice choice options are displayed to the user" + "The order in which multiple-choice options are displayed to the user" ), verbose_name=_("Choice Order"), ) - def check_if_correct(self, guess): - answer = Choice.objects.get(id=guess) + class Meta: + verbose_name = _("Multiple Choice Question") + verbose_name_plural = _("Multiple Choice Questions") - if answer.correct is True: - return True - else: + def check_if_correct(self, guess): + try: + answer = Choice.objects.get(id=int(guess)) + return answer.correct + except (Choice.DoesNotExist, ValueError): return False def order_choices(self, queryset): if self.choice_order == "content": return queryset.order_by("choice") - if self.choice_order == "random": + elif self.choice_order == "random": return queryset.order_by("?") - if self.choice_order == "none": - return queryset.order_by() - return queryset + else: + return queryset def get_choices(self): return self.order_choices(Choice.objects.filter(question=self)) def get_choices_list(self): - return [ - (choice.id, choice.choice) - for choice in self.order_choices(Choice.objects.filter(question=self)) - ] + return [(choice.id, choice.choice_text) for choice in self.get_choices()] def answer_choice_to_string(self, guess): - return Choice.objects.get(id=guess).choice - - class Meta: - verbose_name = _("Multiple Choice Question") - verbose_name_plural = _("Multiple Choice Questions") + try: + return Choice.objects.get(id=int(guess)).choice_text + except (Choice.DoesNotExist, ValueError): + return "" class Choice(models.Model): question = models.ForeignKey( MCQuestion, verbose_name=_("Question"), on_delete=models.CASCADE ) - - choice = models.CharField( + choice_text = models.CharField( max_length=1000, - blank=False, help_text=_("Enter the choice text that you want displayed"), verbose_name=_("Content"), ) - correct = models.BooleanField( - blank=False, default=False, help_text=_("Is this a correct answer?"), verbose_name=_("Correct"), ) - def __str__(self): - return self.choice - class Meta: verbose_name = _("Choice") verbose_name_plural = _("Choices") + def __str__(self): + return self.choice_text + class EssayQuestion(Question): + class Meta: + verbose_name = _("Essay Style Question") + verbose_name_plural = _("Essay Style Questions") + def check_if_correct(self, guess): - return False + return False # Needs manual grading def get_answers(self): return False @@ -556,10 +480,3 @@ def get_answers_list(self): def answer_choice_to_string(self, guess): return str(guess) - - def __str__(self): - return self.content - - class Meta: - verbose_name = _("Essay style question") - verbose_name_plural = _("Essay style questions") diff --git a/quiz/urls.py b/quiz/urls.py index 6c73580..4e42e7e 100644 --- a/quiz/urls.py +++ b/quiz/urls.py @@ -1,23 +1,23 @@ from django.urls import path -from .views import * +from . import views urlpatterns = [ - path("/quizzes/", quiz_list, name="quiz_index"), - path("progress/", view=QuizUserProgressView.as_view(), name="quiz_progress"), + path("/quizzes/", views.quiz_list, name="quiz_index"), + path("progress/", view=views.QuizUserProgressView.as_view(), name="quiz_progress"), # path('marking//', view=QuizMarkingList.as_view(), name='quiz_marking'), - path("marking_list/", view=QuizMarkingList.as_view(), name="quiz_marking"), + path("marking_list/", view=views.QuizMarkingList.as_view(), name="quiz_marking"), path( "marking//", - view=QuizMarkingDetail.as_view(), + view=views.QuizMarkingDetail.as_view(), name="quiz_marking_detail", ), - path("//take/", view=QuizTake.as_view(), name="quiz_take"), - path("/quiz_add/", QuizCreateView.as_view(), name="quiz_create"), - path("//add/", QuizUpdateView.as_view(), name="quiz_update"), - path("//delete/", quiz_delete, name="quiz_delete"), + path("//take/", view=views.QuizTake.as_view(), name="quiz_take"), + path("/quiz_add/", views.QuizCreateView.as_view(), name="quiz_create"), + path("//add/", views.QuizUpdateView.as_view(), name="quiz_update"), + path("//delete/", views.quiz_delete, name="quiz_delete"), path( "mc-question/add///", - MCQuestionCreate.as_view(), + views.MCQuestionCreate.as_view(), name="mc_create", ), # path('mc-question/add///', MCQuestionCreate.as_view(), name='mc_create'), diff --git a/quiz/views.py b/quiz/views.py index c4d75e3..bb4c380 100644 --- a/quiz/views.py +++ b/quiz/views.py @@ -1,126 +1,140 @@ +from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.core.exceptions import PermissionDenied -from django.shortcuts import get_object_or_404, render, redirect +from django.db import transaction +from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator from django.views.generic import ( + CreateView, DetailView, + FormView, ListView, TemplateView, - FormView, - CreateView, UpdateView, ) -from django.contrib import messages -from django.db import transaction from accounts.decorators import lecturer_required -from .models import Course, Progress, Sitting, EssayQuestion, Quiz, MCQuestion, Question from .forms import ( - QuizAddForm, + EssayForm, MCQuestionForm, MCQuestionFormSet, QuestionForm, - EssayForm, + QuizAddForm, +) +from .models import ( + Course, + EssayQuestion, + MCQuestion, + Progress, + Question, + Quiz, + Sitting, ) +# ######################################################## +# Quiz Views +# ######################################################## + + @method_decorator([login_required, lecturer_required], name="dispatch") class QuizCreateView(CreateView): model = Quiz form_class = QuizAddForm + template_name = "quiz/quiz_form.html" - def get_context_data(self, *args, **kwargs): - context = super(QuizCreateView, self).get_context_data(**kwargs) - context["course"] = Course.objects.get(slug=self.kwargs["slug"]) - if self.request.POST: - context["form"] = QuizAddForm(self.request.POST) - # context['quiz'] = self.request.POST.get('quiz') - else: - context["form"] = QuizAddForm( - initial={"course": Course.objects.get(slug=self.kwargs["slug"])} - ) + def get_initial(self): + initial = super().get_initial() + course = get_object_or_404(Course, slug=self.kwargs["slug"]) + initial["course"] = course + return initial + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["course"] = get_object_or_404(Course, slug=self.kwargs["slug"]) return context - def form_valid(self, form, **kwargs): - context = self.get_context_data() - form = context["form"] + def form_valid(self, form): + form.instance.course = get_object_or_404(Course, slug=self.kwargs["slug"]) with transaction.atomic(): self.object = form.save() - if form.is_valid(): - form.instance = self.object - form.save() - return redirect( - "mc_create", slug=self.kwargs["slug"], quiz_id=form.instance.id - ) - return super(QuizCreateView, self).form_invalid(form) + return redirect( + "mc_create", slug=self.kwargs["slug"], quiz_id=self.object.id + ) @method_decorator([login_required, lecturer_required], name="dispatch") class QuizUpdateView(UpdateView): model = Quiz form_class = QuizAddForm + template_name = "quiz/quiz_form.html" - def get_context_data(self, *args, **kwargs): - context = super(QuizUpdateView, self).get_context_data(**kwargs) - context["course"] = Course.objects.get(slug=self.kwargs["slug"]) - quiz = Quiz.objects.get(pk=self.kwargs["pk"]) - if self.request.POST: - context["form"] = QuizAddForm(self.request.POST, instance=quiz) - else: - context["form"] = QuizAddForm(instance=quiz) + def get_object(self, queryset=None): + return get_object_or_404(Quiz, pk=self.kwargs["pk"]) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["course"] = get_object_or_404(Course, slug=self.kwargs["slug"]) return context - def form_valid(self, form, **kwargs): - context = self.get_context_data() - course = context["course"] - form = context["form"] + def form_valid(self, form): with transaction.atomic(): self.object = form.save() - if form.is_valid(): - form.instance = self.object - form.save() - return redirect("quiz_index", course.slug) - return super(QuizUpdateView, self).form_invalid(form) + return redirect("quiz_index", self.kwargs["slug"]) @login_required @lecturer_required def quiz_delete(request, slug, pk): - quiz = Quiz.objects.get(pk=pk) - course = Course.objects.get(slug=slug) + quiz = get_object_or_404(Quiz, pk=pk) quiz.delete() - messages.success(request, f"successfuly deleted.") - return redirect("quiz_index", quiz.course.slug) + messages.success(request, "Quiz successfully deleted.") + return redirect("quiz_index", slug=slug) + + +@login_required +def quiz_list(request, slug): + course = get_object_or_404(Course, slug=slug) + quizzes = Quiz.objects.filter(course=course).order_by("-timestamp") + return render( + request, "quiz/quiz_list.html", {"quizzes": quizzes, "course": course} + ) + + +# ######################################################## +# Multiple Choice Question Views +# ######################################################## @method_decorator([login_required, lecturer_required], name="dispatch") class MCQuestionCreate(CreateView): model = MCQuestion form_class = MCQuestionForm + template_name = "quiz/mcquestion_form.html" + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["quiz"] = get_object_or_404(Quiz, id=self.kwargs["quiz_id"]) + return kwargs def get_context_data(self, **kwargs): - context = super(MCQuestionCreate, self).get_context_data(**kwargs) - context["course"] = Course.objects.get(slug=self.kwargs["slug"]) - context["quiz_obj"] = Quiz.objects.get(id=self.kwargs["quiz_id"]) - context["quizQuestions"] = Question.objects.filter( + context = super().get_context_data(**kwargs) + context["course"] = get_object_or_404(Course, slug=self.kwargs["slug"]) + context["quiz_obj"] = get_object_or_404(Quiz, id=self.kwargs["quiz_id"]) + context["quiz_questions_count"] = Question.objects.filter( quiz=self.kwargs["quiz_id"] ).count() - if self.request.POST: - context["form"] = MCQuestionForm(self.request.POST) + if self.request.method == "POST": context["formset"] = MCQuestionFormSet(self.request.POST) else: - context["form"] = MCQuestionForm(initial={"quiz": self.kwargs["quiz_id"]}) context["formset"] = MCQuestionFormSet() - return context def form_valid(self, form): context = self.get_context_data() formset = context["formset"] - course = context["course"] if formset.is_valid(): with transaction.atomic(): - form.instance.question = self.request.POST.get("content") + form.instance.quiz = get_object_or_404(Quiz, id=self.kwargs["quiz_id"]) self.object = form.save() formset.instance = self.object formset.save() @@ -130,193 +144,131 @@ def form_valid(self, form): slug=self.kwargs["slug"], quiz_id=self.kwargs["quiz_id"], ) - return redirect("quiz_index", course.slug) + return redirect("quiz_index", slug=self.kwargs["slug"]) else: return self.form_invalid(form) - return super(MCQuestionCreate, self).form_invalid(form) - - -@login_required -def quiz_list(request, slug): - quizzes = Quiz.objects.filter(course__slug=slug).order_by("-timestamp") - course = Course.objects.get(slug=slug) - return render( - request, "quiz/quiz_list.html", {"quizzes": quizzes, "course": course} - ) - # return render(request, 'quiz/quiz_list.html', {'quizzes': quizzes}) - - -@method_decorator([login_required, lecturer_required], name="dispatch") -class QuizMarkerMixin(object): - @method_decorator(login_required) - # @method_decorator(permission_required('quiz.view_sittings')) - def dispatch(self, *args, **kwargs): - return super(QuizMarkerMixin, self).dispatch(*args, **kwargs) - -# @method_decorator([login_required, lecturer_required], name='get_queryset') -class SittingFilterTitleMixin(object): - def get_queryset(self): - queryset = super(SittingFilterTitleMixin, self).get_queryset() - quiz_filter = self.request.GET.get("quiz_filter") - if quiz_filter: - queryset = queryset.filter(quiz__title__icontains=quiz_filter) - return queryset +# ######################################################## +# Quiz Progress and Marking Views +# ######################################################## @method_decorator([login_required], name="dispatch") class QuizUserProgressView(TemplateView): - template_name = "progress.html" - - def dispatch(self, request, *args, **kwargs): - return super(QuizUserProgressView, self).dispatch(request, *args, **kwargs) + template_name = "quiz/progress.html" def get_context_data(self, **kwargs): - context = super(QuizUserProgressView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) progress, _ = Progress.objects.get_or_create(user=self.request.user) context["cat_scores"] = progress.list_all_cat_scores context["exams"] = progress.show_exams() - context["exams_counter"] = progress.show_exams().count() + context["exams_counter"] = context["exams"].count() return context @method_decorator([login_required, lecturer_required], name="dispatch") -class QuizMarkingList(QuizMarkerMixin, SittingFilterTitleMixin, ListView): +class QuizMarkingList(ListView): model = Sitting + template_name = "quiz/quiz_marking_list.html" - # def get_context_data(self, **kwargs): - # context = super(QuizMarkingList, self).get_context_data(**kwargs) - # context['queryset_counter'] = super(QuizMarkingList, self).get_queryset().filter(complete=True).filter(course__allocated_course__lecturer__pk=self.request.user.id).count() - # context['marking_list'] = super(QuizMarkingList, self).get_queryset().filter(complete=True).filter(course__allocated_course__lecturer__pk=self.request.user.id) - # return context def get_queryset(self): - if self.request.user.is_superuser: - queryset = super(QuizMarkingList, self).get_queryset().filter(complete=True) - else: - queryset = ( - super(QuizMarkingList, self) - .get_queryset() - .filter( - quiz__course__allocated_course__lecturer__pk=self.request.user.id - ) - .filter(complete=True) + queryset = Sitting.objects.filter(complete=True) + if not self.request.user.is_superuser: + queryset = queryset.filter( + quiz__course__allocated_course__lecturer__pk=self.request.user.id ) - - # search by user + quiz_filter = self.request.GET.get("quiz_filter") + if quiz_filter: + queryset = queryset.filter(quiz__title__icontains=quiz_filter) user_filter = self.request.GET.get("user_filter") if user_filter: queryset = queryset.filter(user__username__icontains=user_filter) - return queryset @method_decorator([login_required, lecturer_required], name="dispatch") -class QuizMarkingDetail(QuizMarkerMixin, DetailView): +class QuizMarkingDetail(DetailView): model = Sitting + template_name = "quiz/quiz_marking_detail.html" def post(self, request, *args, **kwargs): sitting = self.get_object() - - q_to_toggle = request.POST.get("qid", None) - if q_to_toggle: - q = Question.objects.get_subclass(id=int(q_to_toggle)) - if int(q_to_toggle) in sitting.get_incorrect_questions: - sitting.remove_incorrect_question(q) + question_id = request.POST.get("qid") + if question_id: + question = Question.objects.get_subclass(id=int(question_id)) + if int(question_id) in sitting.get_incorrect_questions: + sitting.remove_incorrect_question(question) else: - sitting.add_incorrect_question(q) - - return self.get(request) + sitting.add_incorrect_question(question) + return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): - context = super(QuizMarkingDetail, self).get_context_data(**kwargs) - context["questions"] = context["sitting"].get_questions(with_answers=True) + context = super().get_context_data(**kwargs) + context["questions"] = self.object.get_questions(with_answers=True) return context -# @method_decorator([login_required, student_required], name='dispatch') +# ######################################################## +# Quiz Taking View +# ######################################################## + + @method_decorator([login_required], name="dispatch") class QuizTake(FormView): form_class = QuestionForm - template_name = "question.html" - result_template_name = "result.html" - # single_complete_template_name = 'single_complete.html' + template_name = "quiz/question.html" + result_template_name = "quiz/result.html" def dispatch(self, request, *args, **kwargs): self.quiz = get_object_or_404(Quiz, slug=self.kwargs["slug"]) self.course = get_object_or_404(Course, pk=self.kwargs["pk"]) - quizQuestions = Question.objects.filter(quiz=self.quiz).count() - - if quizQuestions <= 0: - messages.warning(request, f"Question set of the quiz is empty. try later!") - return redirect("quiz_index", self.course.slug) - - # if self.quiz.draft and not request.user.has_perm("quiz.change_quiz"): - # raise PermissionDenied + if not Question.objects.filter(quiz=self.quiz).exists(): + messages.warning(request, "This quiz has no questions available.") + return redirect("quiz_index", slug=self.course.slug) self.sitting = Sitting.objects.user_sitting( request.user, self.quiz, self.course ) - - if self.sitting is False: - # return render(request, self.single_complete_template_name) + if not self.sitting: messages.info( request, - f"You have already sat this exam and only one sitting is permitted", + "You have already completed this quiz. Only one attempt is permitted.", ) - return redirect("quiz_index", self.course.slug) - - return super(QuizTake, self).dispatch(request, *args, **kwargs) + return redirect("quiz_index", slug=self.course.slug) + return super().dispatch(request, *args, **kwargs) - def get_form(self, *args, **kwargs): + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() self.question = self.sitting.get_first_question() self.progress = self.sitting.progress() + kwargs["question"] = self.question + return kwargs - if self.question.__class__ is EssayQuestion: - form_class = EssayForm - else: - form_class = self.form_class - - return form_class(**self.get_form_kwargs()) - - def get_form_kwargs(self): - kwargs = super(QuizTake, self).get_form_kwargs() - - return dict(kwargs, question=self.question) + def get_form_class(self): + if isinstance(self.question, EssayQuestion): + return EssayForm + return self.form_class def form_valid(self, form): self.form_valid_user(form) - if self.sitting.get_first_question() is False: + if not self.sitting.get_first_question(): return self.final_result_user() - - self.request.POST = {} - - return super(QuizTake, self).get(self, self.request) - - def get_context_data(self, **kwargs): - context = super(QuizTake, self).get_context_data(**kwargs) - context["question"] = self.question - context["quiz"] = self.quiz - context["course"] = get_object_or_404(Course, pk=self.kwargs["pk"]) - if hasattr(self, "previous"): - context["previous"] = self.previous - if hasattr(self, "progress"): - context["progress"] = self.progress - return context + return super().get(self.request) def form_valid_user(self, form): progress, _ = Progress.objects.get_or_create(user=self.request.user) guess = form.cleaned_data["answers"] is_correct = self.question.check_if_correct(guess) - if is_correct is True: + if is_correct: self.sitting.add_to_score(1) progress.update_score(self.question, 1, 1) else: self.sitting.add_incorrect_question(self.question) progress.update_score(self.question, 0, 1) - if self.quiz.answers_at_end is not True: + if not self.quiz.answers_at_end: self.previous = { "previous_answer": guess, "previous_outcome": is_correct, @@ -330,26 +282,35 @@ def form_valid_user(self, form): self.sitting.add_user_answer(self.question, guess) self.sitting.remove_first_question() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["question"] = self.question + context["quiz"] = self.quiz + context["course"] = self.course + if hasattr(self, "previous"): + context["previous"] = self.previous + if hasattr(self, "progress"): + context["progress"] = self.progress + return context + def final_result_user(self): + self.sitting.mark_quiz_complete() results = { - "course": get_object_or_404(Course, pk=self.kwargs["pk"]), + "course": self.course, "quiz": self.quiz, "score": self.sitting.get_current_score, "max_score": self.sitting.get_max_score, "percent": self.sitting.get_percent_correct, "sitting": self.sitting, - "previous": self.previous, - "course": get_object_or_404(Course, pk=self.kwargs["pk"]), + "previous": getattr(self, "previous", {}), } - self.sitting.mark_quiz_complete() - if self.quiz.answers_at_end: results["questions"] = self.sitting.get_questions(with_answers=True) results["incorrect_questions"] = self.sitting.get_incorrect_questions if ( - self.quiz.exam_paper is False + not self.quiz.exam_paper or self.request.user.is_superuser or self.request.user.is_lecturer ): From 27d24a9b2892befaa537c14a771e91ffe668bd2d Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 01:22:09 +0300 Subject: [PATCH 03/17] Follow pylint rule for result app --- result/models.py | 322 +++++++++++++++++++---------------------------- result/views.py | 5 - 2 files changed, 126 insertions(+), 201 deletions(-) diff --git a/result/models.py b/result/models.py index e857c4c..d60a546 100644 --- a/result/models.py +++ b/result/models.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from django.db import models from django.urls import reverse @@ -5,22 +7,21 @@ from core.models import Semester from course.models import Course +# Constants YEARS = ( (1, "1"), (2, "2"), (3, "3"), (4, "4"), - (4, "5"), - (4, "6"), + (5, "5"), + (6, "6"), ) -# LEVEL_COURSE = "Level course" -BACHLOAR_DEGREE = "Bachloar" +BACHELOR_DEGREE = "Bachelor" MASTER_DEGREE = "Master" -LEVEL = ( - # (LEVEL_COURSE, "Level course"), - (BACHLOAR_DEGREE, "Bachloar Degree"), +LEVEL_CHOICES = ( + (BACHELOR_DEGREE, "Bachelor Degree"), (MASTER_DEGREE, "Master Degree"), ) @@ -28,7 +29,7 @@ SECOND = "Second" THIRD = "Third" -SEMESTER = ( +SEMESTER_CHOICES = ( (FIRST, "First"), (SECOND, "Second"), (THIRD, "Third"), @@ -47,7 +48,7 @@ F = "F" NG = "NG" -GRADE = ( +GRADE_CHOICES = ( (A_PLUS, "A+"), (A, "A"), (A_MINUS, "A-"), @@ -65,19 +66,39 @@ PASS = "PASS" FAIL = "FAIL" -COMMENT = ( +COMMENT_CHOICES = ( (PASS, "PASS"), (FAIL, "FAIL"), ) - -class TakenCourseManager(models.Manager): - def new(self, user=None): - user_obj = None - if user is not None: - if user.is_authenticated(): - user_obj = user - return self.model.objects.create(user=user_obj) +GRADE_BOUNDARIES = [ + (90, A_PLUS), + (85, A), + (80, A_MINUS), + (75, B_PLUS), + (70, B), + (65, B_MINUS), + (60, C_PLUS), + (55, C), + (50, C_MINUS), + (45, D), + (0, F), +] + +GRADE_POINT_MAPPING = { + A_PLUS: 4.0, + A: 4.0, + A_MINUS: 3.75, + B_PLUS: 3.5, + B: 3.0, + B_MINUS: 2.75, + C_PLUS: 2.5, + C: 2.0, + C_MINUS: 1.75, + D: 1.0, + F: 0.0, + NG: 0.0, +} class TakenCourse(models.Model): @@ -85,202 +106,111 @@ class TakenCourse(models.Model): course = models.ForeignKey( Course, on_delete=models.CASCADE, related_name="taken_courses" ) - assignment = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - mid_exam = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - quiz = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - attendance = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - final_exam = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - total = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - grade = models.CharField(choices=GRADE, max_length=2, blank=True) - point = models.DecimalField(max_digits=5, decimal_places=2, default=0.0) - comment = models.CharField(choices=COMMENT, max_length=200, blank=True) + assignment = models.DecimalField( + max_digits=5, decimal_places=2, default=Decimal("0.00") + ) + mid_exam = models.DecimalField( + max_digits=5, decimal_places=2, default=Decimal("0.00") + ) + quiz = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00")) + attendance = models.DecimalField( + max_digits=5, decimal_places=2, default=Decimal("0.00") + ) + final_exam = models.DecimalField( + max_digits=5, decimal_places=2, default=Decimal("0.00") + ) + total = models.DecimalField( + max_digits=5, decimal_places=2, default=Decimal("0.00"), editable=False + ) + grade = models.CharField( + choices=GRADE_CHOICES, max_length=2, blank=True, editable=False + ) + point = models.DecimalField( + max_digits=5, decimal_places=2, default=Decimal("0.00"), editable=False + ) + comment = models.CharField( + choices=COMMENT_CHOICES, max_length=200, blank=True, editable=False + ) def get_absolute_url(self): return reverse("course_detail", kwargs={"slug": self.course.slug}) def __str__(self): - return "{0} ({1})".format(self.course.title, self.course.code) - - # @staticmethod - def get_total(self, assignment, mid_exam, quiz, attendance, final_exam): - return ( - float(assignment) - + float(mid_exam) - + float(quiz) - + float(attendance) - + float(final_exam) + return f"{self.course.title} ({self.course.code})" + + def get_total(self): + return sum( + [ + self.assignment, + self.mid_exam, + self.quiz, + self.attendance, + self.final_exam, + ] ) - # @staticmethod - def get_grade(self, total): - # total = float(assignment) + float(mid_exam) + float(quiz) + float(attendance) + float(final_exam) - # total = self.get_total(assignment=assignment, mid_exam=mid_exam, quiz=quiz, attendance=attendance, final_exam=final_exam) - # total = total - if total >= 90: - grade = A_PLUS - elif total >= 85: - grade = A - elif total >= 80: - grade = A_MINUS - elif total >= 75: - grade = B_PLUS - elif total >= 70: - grade = B - elif total >= 65: - grade = B_MINUS - elif total >= 60: - grade = C_PLUS - elif total >= 55: - grade = C - elif total >= 50: - grade = C_MINUS - elif total >= 45: - grade = D - elif total < 45: - grade = F - else: - grade = NG - return grade - - # @staticmethod - def get_comment(self, grade): - if grade == F or grade == NG: - comment = FAIL - # elif grade == NG: - # comment = FAIL - else: - comment = PASS - return comment - - def get_point(self, grade): - p = 0 - # point = 0 - # for i in student: + def get_grade(self): + total = self.total + for boundary, grade in GRADE_BOUNDARIES: + if total >= boundary: + return grade + return NG + + def get_comment(self): + if self.grade in [F, NG]: + return FAIL + return PASS + + def get_point(self): credit = self.course.credit - if self.grade == A_PLUS: - point = 4 - elif self.grade == A: - point = 4 - elif self.grade == A_MINUS: - point = 3.75 - elif self.grade == B_PLUS: - point = 3.5 - elif self.grade == B: - point = 3 - elif self.grade == B_MINUS: - point = 2.75 - elif self.grade == C_PLUS: - point = 2.5 - elif self.grade == C: - point = 2 - elif self.grade == C_MINUS: - point = 1.75 - elif self.grade == D: - point = 1 - else: - point = 0 - p += int(credit) * point - return p - - def calculate_gpa(self, total_credit_in_semester): - current_semester = Semester.objects.get(is_current_semester=True) - student = TakenCourse.objects.filter( + grade_point = GRADE_POINT_MAPPING.get(self.grade, 0.0) + return Decimal(credit) * Decimal(grade_point) + + def save(self, *args, **kwargs): + self.total = self.get_total() + self.grade = self.get_grade() + self.point = self.get_point() + self.comment = self.get_comment() + super().save(*args, **kwargs) + + def calculate_gpa(self): + current_semester = Semester.objects.filter(is_current_semester=True).first() + if not current_semester: + return Decimal("0.00") + + taken_courses = TakenCourse.objects.filter( student=self.student, course__level=self.student.level, - course__semester=current_semester, + course__semester=current_semester.semester, ) - p = 0 - point = 0 - for i in student: - credit = i.course.credit - if i.grade == A_PLUS: - point = 4 - elif i.grade == A: - point = 4 - elif i.grade == A_MINUS: - point = 3.75 - elif i.grade == B_PLUS: - point = 3.5 - elif i.grade == B: - point = 3 - elif i.grade == B_MINUS: - point = 2.75 - elif i.grade == C_PLUS: - point = 2.5 - elif i.grade == C: - point = 2 - elif i.grade == C_MINUS: - point = 1.75 - elif i.grade == D: - point = 1 - else: - point = 0 - p += int(credit) * point - try: - gpa = p / total_credit_in_semester + + total_points = sum(tc.point for tc in taken_courses) + total_credits = sum(tc.course.credit for tc in taken_courses) + + if total_credits > 0: + gpa = total_points / Decimal(total_credits) return round(gpa, 2) - except ZeroDivisionError: - return 0 + return Decimal("0.00") def calculate_cgpa(self): - current_semester = Semester.objects.get(is_current_semester=True) - previousResult = Result.objects.filter( - student__id=self.student.id, level__lt=self.student.level - ) - previous_cgpa = 0 - for i in previousResult: - if i.cgpa is not None: - previous_cgpa += i.cgpa - cgpa = 0 - if str(current_semester) == SECOND: - first_sem_gpa = 0.0 - sec_sem_gpa = 0.0 - try: - first_sem_result = Result.objects.get( - student=self.student.id, semester=FIRST, level=self.student.level - ) - first_sem_gpa += first_sem_result.gpa - except: - first_sem_gpa = 0 - - try: - sec_sem_result = Result.objects.get( - student=self.student.id, semester=SECOND, level=self.student.level - ) - sec_sem_gpa += sec_sem_result.gpa - except: - sec_sem_gpa = 0 - - taken_courses = TakenCourse.objects.filter( - student=self.student, student__level=self.student.level - ) - taken_course_credits = 0 - taken_course_points = 0 - for i in taken_courses: - taken_course_points += float(i.point) - for i in taken_courses: - taken_course_credits += int(i.course.credit) - # cgpa = (first_sem_gpa + sec_sem_gpa) / 2 - - print("taken_course_points = ", taken_course_points) - print("taken_course_credits = ", taken_course_credits) - print("first_sem_gpa = ", first_sem_gpa) - print("sec_sem_gpa = ", sec_sem_gpa) - print("cgpa = ", round(taken_course_points / taken_course_credits, 2)) - - try: - cgpa = taken_course_points / taken_course_credits - return round(cgpa, 2) - except ZeroDivisionError: - return 0 - - # return round(cgpa, 2) + taken_courses = TakenCourse.objects.filter(student=self.student) + + total_points = sum(tc.point for tc in taken_courses) + total_credits = sum(tc.course.credit for tc in taken_courses) + + if total_credits > 0: + cgpa = total_points / Decimal(total_credits) + return round(cgpa, 2) + return Decimal("0.00") class Result(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE) gpa = models.FloatField(null=True) cgpa = models.FloatField(null=True) - semester = models.CharField(max_length=100, choices=SEMESTER) + semester = models.CharField(max_length=100, choices=SEMESTER_CHOICES) session = models.CharField(max_length=100, blank=True, null=True) - level = models.CharField(max_length=25, choices=LEVEL, null=True) + level = models.CharField(max_length=25, choices=LEVEL_CHOICES, null=True) + + def __str__(self): + return f"Result for {self.student} - Semester: {self.semester}, Level: {self.level}" diff --git a/result/views.py b/result/views.py index 2a2f5e1..edb29af 100644 --- a/result/views.py +++ b/result/views.py @@ -456,7 +456,6 @@ def result_sheet_pdf_view(request, id): @login_required @student_required def course_registration_form(request): - current_semester = Semester.objects.get(is_current_semester=True) current_session = Session.objects.get(is_current_session=True) courses = TakenCourse.objects.filter(student__student__id=request.user.id) fname = request.user.username + ".pdf" @@ -514,7 +513,6 @@ def course_registration_form(request): Story.append(title) student = Student.objects.get(student__pk=request.user.id) - style_right = ParagraphStyle(name="right", parent=styles["Normal"]) tbl_data = [ [ Paragraph( @@ -550,8 +548,6 @@ def course_registration_form(request): semester_title = Paragraph(semester_title, semester) Story.append(semester_title) - elements = [] - # FIRST SEMESTER count = 0 header = [ @@ -598,7 +594,6 @@ def course_registration_form(request): "", ) ] - color = colors.black count += 1 table_body = Table(data, 1 * [1.4 * inch], 1 * [0.3 * inch]) table_body.setStyle( From 73cfe06c583caf5228ca50bb993d9ec04be4e80e Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 01:46:58 +0300 Subject: [PATCH 04/17] Fix: unknown field choice --- accounts/apps.py | 7 ++++--- accounts/signals.py | 2 +- quiz/forms.py | 29 ++++++++++++++++++----------- quiz/models.py | 1 + quiz/translation.py | 24 +++++++++++++++++------- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/accounts/apps.py b/accounts/apps.py index e9ee8c6..5f93240 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -1,13 +1,14 @@ 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/signals.py b/accounts/signals.py index 9194683..6a4cfa0 100644 --- a/accounts/signals.py +++ b/accounts/signals.py @@ -5,7 +5,7 @@ ) -def post_save_account_receiver(instance=None, created=False): +def post_save_account_receiver(instance=None, created=False, *args, **kwargs): """ Send email notification """ diff --git a/quiz/forms.py b/quiz/forms.py index 4bbbced..b54448c 100644 --- a/quiz/forms.py +++ b/quiz/forms.py @@ -42,9 +42,9 @@ class Meta: def __init__(self, *args, **kwargs): super(QuizAddForm, self).__init__(*args, **kwargs) if self.instance.pk: - self.fields[ - "questions" - ].initial = self.instance.question_set.all().select_subclasses() + self.fields["questions"].initial = ( + self.instance.question_set.all().select_subclasses() + ) def save(self, commit=True): quiz = super(QuizAddForm, self).save(commit=False) @@ -59,6 +59,7 @@ class Meta: model = MCQuestion exclude = () + class MCQuestionFormSet(forms.BaseInlineFormSet): def clean(self): """ @@ -69,10 +70,14 @@ def clean(self): super().clean() # Collect non-deleted forms - valid_forms = [form for form in self.forms if not form.cleaned_data.get('DELETE', True)] - - valid_choices = ['choice' in form.cleaned_data.keys() for form in valid_forms] - if(not all(valid_choices)): + valid_forms = [ + form for form in self.forms if not form.cleaned_data.get("DELETE", True) + ] + + valid_choices = [ + "choice_text" in form.cleaned_data.keys() for form in valid_forms + ] + if not all(valid_choices): raise forms.ValidationError("You must add a valid choice name.") # If all forms are deleted, raise a validation error @@ -80,12 +85,14 @@ def clean(self): raise forms.ValidationError("You must provide at least two choices.") # Check if at least one of the valid forms is marked as correct - correct_choices = [form.cleaned_data.get('correct', False) for form in valid_forms] + correct_choices = [ + form.cleaned_data.get("correct", False) for form in valid_forms + ] if not any(correct_choices): raise forms.ValidationError("One choice must be marked as correct.") - - if correct_choices.count(True)>1: + + if correct_choices.count(True) > 1: raise forms.ValidationError("Only one choice must be marked as correct.") @@ -94,7 +101,7 @@ def clean(self): Choice, form=MCQuestionForm, formset=MCQuestionFormSet, - fields=["choice", "correct"], + fields=["choice_text", "correct"], can_delete=True, extra=5, ) diff --git a/quiz/models.py b/quiz/models.py index 2600b94..97b6e57 100644 --- a/quiz/models.py +++ b/quiz/models.py @@ -13,6 +13,7 @@ from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ +from django.dispatch import receiver from model_utils.managers import InheritanceManager from course.models import Course diff --git a/quiz/translation.py b/quiz/translation.py index de6a8e4..cce9c51 100644 --- a/quiz/translation.py +++ b/quiz/translation.py @@ -1,21 +1,31 @@ from modeltranslation.translator import register, TranslationOptions from .models import Quiz, Question, Choice, MCQuestion + @register(Quiz) class QuizTranslationOptions(TranslationOptions): - fields = ('title', 'description',) - empty_values=None + fields = ( + "title", + "description", + ) + empty_values = None + @register(Question) class QuestionTranslationOptions(TranslationOptions): - fields = ('content', 'explanation',) - empty_values=None + fields = ( + "content", + "explanation", + ) + empty_values = None + @register(Choice) class ChoiceTranslationOptions(TranslationOptions): - fields = ('choice',) - empty_values=None + fields = ("choice_text",) + empty_values = None + @register(MCQuestion) class MCQuestionTranslationOptions(TranslationOptions): - pass \ No newline at end of file + pass From aa43040c304cb896be77c6367874ff92a1890cd0 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 01:58:34 +0300 Subject: [PATCH 05/17] Rename few model fields --- ...ourse_code_alter_course_credit_and_more.py | 96 +++++++++++++ ...04_alter_essayquestion_options_and_more.py | 130 ++++++++++++++++++ ...evel_alter_takencourse_comment_and_more.py | 70 ++++++++++ templates/{ => quiz}/progress.html | 0 4 files changed, 296 insertions(+) create mode 100644 course/migrations/0004_alter_course_code_alter_course_credit_and_more.py create mode 100644 quiz/migrations/0004_alter_essayquestion_options_and_more.py create mode 100644 result/migrations/0002_alter_result_level_alter_takencourse_comment_and_more.py rename templates/{ => quiz}/progress.html (100%) diff --git a/course/migrations/0004_alter_course_code_alter_course_credit_and_more.py b/course/migrations/0004_alter_course_code_alter_course_credit_and_more.py new file mode 100644 index 0000000..e4daff8 --- /dev/null +++ b/course/migrations/0004_alter_course_code_alter_course_credit_and_more.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.16 on 2024-10-04 22:51 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0003_course_summary_es_course_summary_fr_course_title_es_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="course", + name="code", + field=models.CharField(default="testcode101", max_length=200, unique=True), + preserve_default=False, + ), + migrations.AlterField( + model_name="course", + name="credit", + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name="course", + name="is_elective", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="course", + name="level", + field=models.CharField( + choices=[("Bachelor", "Bachelor Degree"), ("Master", "Master Degree")], + default="Bachelor", + max_length=25, + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="course", + name="summary", + field=models.TextField(blank=True, default="Test summary", max_length=200), + preserve_default=False, + ), + migrations.AlterField( + model_name="course", + name="title", + field=models.CharField(default="Test title", max_length=200), + preserve_default=False, + ), + migrations.AlterField( + model_name="course", + name="year", + field=models.IntegerField( + choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "5"), (6, "6")], + default=1, + ), + ), + migrations.AlterField( + model_name="program", + name="summary", + field=models.TextField(blank=True, default="Test summary"), + preserve_default=False, + ), + migrations.AlterField( + model_name="upload", + name="updated_date", + field=models.DateTimeField( + auto_now=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="upload", + name="upload_time", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="uploadvideo", + name="summary", + field=models.TextField(blank=True, default="test"), + preserve_default=False, + ), + migrations.AlterField( + model_name="uploadvideo", + name="timestamp", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + ] diff --git a/quiz/migrations/0004_alter_essayquestion_options_and_more.py b/quiz/migrations/0004_alter_essayquestion_options_and_more.py new file mode 100644 index 0000000..bedaf3d --- /dev/null +++ b/quiz/migrations/0004_alter_essayquestion_options_and_more.py @@ -0,0 +1,130 @@ +# Generated by Django 4.2.16 on 2024-10-04 22:51 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0004_alter_course_code_alter_course_credit_and_more"), + ("quiz", "0003_choice_choice_es_choice_choice_fr_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="essayquestion", + options={ + "verbose_name": "Essay Style Question", + "verbose_name_plural": "Essay Style Questions", + }, + ), + migrations.RenameField( + model_name="choice", + old_name="choice", + new_name="choice_text", + ), + migrations.RenameField( + model_name="choice", + old_name="choice_en", + new_name="choice_text_en", + ), + migrations.RenameField( + model_name="choice", + old_name="choice_es", + new_name="choice_text_es", + ), + migrations.RenameField( + model_name="choice", + old_name="choice_fr", + new_name="choice_text_fr", + ), + migrations.RenameField( + model_name="choice", + old_name="choice_ru", + new_name="choice_text_ru", + ), + migrations.AlterField( + model_name="mcquestion", + name="choice_order", + field=models.CharField( + blank=True, + choices=[ + ("content", "Content"), + ("random", "Random"), + ("none", "None"), + ], + default="random", + help_text="The order in which multiple-choice options are displayed to the user", + max_length=30, + verbose_name="Choice Order", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="question", + name="figure", + field=models.ImageField( + blank=True, + default="test", + help_text="Add an image for the question if necessary.", + upload_to="uploads/%Y/%m/%d", + verbose_name="Figure", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="quiz", + name="category", + field=models.CharField( + blank=True, + choices=[ + ("assignment", "Assignment"), + ("exam", "Exam"), + ("practice", "Practice Quiz"), + ], + max_length=20, + ), + ), + migrations.AlterField( + model_name="quiz", + name="course", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to="course.course", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="quiz", + name="draft", + field=models.BooleanField( + default=False, + help_text="If yes, the quiz is not displayed in the quiz list and can only be taken by users who can edit quizzes.", + verbose_name="Draft", + ), + ), + migrations.AlterField( + model_name="quiz", + name="pass_mark", + field=models.SmallIntegerField( + default=50, + help_text="Percentage required to pass exam.", + validators=[django.core.validators.MaxValueValidator(100)], + verbose_name="Pass Mark", + ), + ), + migrations.AlterField( + model_name="sitting", + name="course", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to="course.course", + verbose_name="Course", + ), + preserve_default=False, + ), + ] diff --git a/result/migrations/0002_alter_result_level_alter_takencourse_comment_and_more.py b/result/migrations/0002_alter_result_level_alter_takencourse_comment_and_more.py new file mode 100644 index 0000000..22f84d7 --- /dev/null +++ b/result/migrations/0002_alter_result_level_alter_takencourse_comment_and_more.py @@ -0,0 +1,70 @@ +# Generated by Django 4.2.16 on 2024-10-04 22:51 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("result", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="result", + name="level", + field=models.CharField( + choices=[("Bachelor", "Bachelor Degree"), ("Master", "Master Degree")], + max_length=25, + null=True, + ), + ), + migrations.AlterField( + model_name="takencourse", + name="comment", + field=models.CharField( + blank=True, + choices=[("PASS", "PASS"), ("FAIL", "FAIL")], + editable=False, + max_length=200, + ), + ), + migrations.AlterField( + model_name="takencourse", + name="grade", + field=models.CharField( + blank=True, + choices=[ + ("A+", "A+"), + ("A", "A"), + ("A-", "A-"), + ("B+", "B+"), + ("B", "B"), + ("B-", "B-"), + ("C+", "C+"), + ("C", "C"), + ("C-", "C-"), + ("D", "D"), + ("F", "F"), + ("NG", "NG"), + ], + editable=False, + max_length=2, + ), + ), + migrations.AlterField( + model_name="takencourse", + name="point", + field=models.DecimalField( + decimal_places=2, default=Decimal("0.00"), editable=False, max_digits=5 + ), + ), + migrations.AlterField( + model_name="takencourse", + name="total", + field=models.DecimalField( + decimal_places=2, default=Decimal("0.00"), editable=False, max_digits=5 + ), + ), + ] diff --git a/templates/progress.html b/templates/quiz/progress.html similarity index 100% rename from templates/progress.html rename to templates/quiz/progress.html From e88d73565fe37f0cb7bc326de4155eba83f65155 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 02:04:09 +0300 Subject: [PATCH 06/17] Fix: template does not exist --- quiz/templatetags/quiz_tags.py | 7 +++---- templates/{ => quiz}/correct_answer.html | 0 templates/{ => quiz}/question.html | 0 templates/{ => quiz}/result.html | 0 4 files changed, 3 insertions(+), 4 deletions(-) rename templates/{ => quiz}/correct_answer.html (100%) rename templates/{ => quiz}/question.html (100%) rename templates/{ => quiz}/result.html (100%) diff --git a/quiz/templatetags/quiz_tags.py b/quiz/templatetags/quiz_tags.py index d256bb0..c5acced 100644 --- a/quiz/templatetags/quiz_tags.py +++ b/quiz/templatetags/quiz_tags.py @@ -3,21 +3,20 @@ register = template.Library() -@register.inclusion_tag('correct_answer.html', takes_context=True) +@register.inclusion_tag("quiz/correct_answer.html", takes_context=True) def correct_answer_for_all(context, question): """ processes the correct answer based on a given question object if the answer is incorrect, informs the user """ answers = question.get_choices() - incorrect_list = context.get('incorrect_questions', []) + incorrect_list = context.get("incorrect_questions", []) if question.id in incorrect_list: user_was_incorrect = True else: user_was_incorrect = False - return {'previous': {'answers': answers}, - 'user_was_incorrect': user_was_incorrect} + return {"previous": {"answers": answers}, "user_was_incorrect": user_was_incorrect} @register.filter diff --git a/templates/correct_answer.html b/templates/quiz/correct_answer.html similarity index 100% rename from templates/correct_answer.html rename to templates/quiz/correct_answer.html diff --git a/templates/question.html b/templates/quiz/question.html similarity index 100% rename from templates/question.html rename to templates/quiz/question.html diff --git a/templates/result.html b/templates/quiz/result.html similarity index 100% rename from templates/result.html rename to templates/quiz/result.html From 4957751101287565ff16087f0031da9c64195917 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 02:33:43 +0300 Subject: [PATCH 07/17] Fix: pylint rules --- .pylintrc | 3 ++- config/settings.py | 30 ++++++++++++++++++++++++++++++ core/utils.py | 28 ++++++++++++++++++++++++++++ course/models.py | 39 +++++---------------------------------- course/utils.py | 30 ------------------------------ quiz/forms.py | 4 ---- quiz/models.py | 2 +- quiz/utils.py | 30 ------------------------------ result/models.py | 33 +++------------------------------ 9 files changed, 69 insertions(+), 130 deletions(-) diff --git a/.pylintrc b/.pylintrc index 99d3a19..c27897b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -451,7 +451,8 @@ disable=raw-checker-failed, arguments-differ, invalid-overridden-method, unsupported-binary-operation, - attribute-defined-outside-init + attribute-defined-outside-init, + duplicate-code # 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 diff --git a/config/settings.py b/config/settings.py index cc45d3a..7f03b05 100644 --- a/config/settings.py +++ b/config/settings.py @@ -12,6 +12,7 @@ import os from decouple import config +from django.utils.translation import gettext_lazy as _ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -251,3 +252,32 @@ def gettext(s): STUDENT_ID_PREFIX = config("STUDENT_ID_PREFIX", "ugr") LECTURER_ID_PREFIX = config("LECTURER_ID_PREFIX", "lec") + + +# Constants +YEARS = ( + (1, "1"), + (2, "2"), + (3, "3"), + (4, "4"), + (5, "5"), + (6, "6"), +) + +BACHELOR_DEGREE = "Bachelor" +MASTER_DEGREE = "Master" + +LEVEL_CHOICES = ( + (BACHELOR_DEGREE, _("Bachelor Degree")), + (MASTER_DEGREE, _("Master Degree")), +) + +FIRST = "First" +SECOND = "Second" +THIRD = "Third" + +SEMESTER_CHOICES = ( + (FIRST, _("First")), + (SECOND, _("Second")), + (THIRD, _("Third")), +) diff --git a/core/utils.py b/core/utils.py index 19bf670..5bd2d5d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,7 +1,11 @@ +import random +import string +from django.utils.text import slugify from django.core.mail import send_mail from django.template.loader import render_to_string from django.utils.html import strip_tags from django.conf import settings +from django.utils.text import slugify def send_email(user, subject, msg): @@ -30,3 +34,27 @@ def send_html_email(subject, recipient_list, template, context): recipient_list, html_message=html_message, ) + + +def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits): + return "".join(random.choice(chars) for _ in range(size)) + + +def unique_slug_generator(instance, new_slug=None): + """ + Assumes the instance has a model with a slug field and a title + character (char) field. + """ + if new_slug is not None: + slug = new_slug + else: + slug = slugify(instance.title) + + Klass = instance.__class__ + qs_exists = Klass.objects.filter(slug=slug).exists() + if qs_exists: + new_slug = "{slug}-{randstr}".format( + slug=slug, randstr=random_string_generator(size=4) + ) + return unique_slug_generator(instance, new_slug=new_slug) + return slug diff --git a/course/models.py b/course/models.py index 89c900e..3737745 100644 --- a/course/models.py +++ b/course/models.py @@ -7,37 +7,8 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ -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"), - (5, "5"), - (6, "6"), -) - -BACHELOR_DEGREE = "Bachelor" -MASTER_DEGREE = "Master" - -LEVEL_CHOICES = ( - (BACHELOR_DEGREE, _("Bachelor Degree")), - (MASTER_DEGREE, _("Master Degree")), -) - -FIRST = "First" -SECOND = "Second" -THIRD = "Third" - -SEMESTER_CHOICES = ( - (FIRST, _("First")), - (SECOND, _("Second")), - (THIRD, _("Third")), -) +from core.models import ActivityLog, Semester +from core.utils import unique_slug_generator class ProgramManager(models.Manager): @@ -94,9 +65,9 @@ class Course(models.Model): 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_CHOICES) - year = models.IntegerField(choices=YEARS, default=1) - semester = models.CharField(choices=SEMESTER_CHOICES, max_length=200) + level = models.CharField(max_length=25, choices=settings.LEVEL_CHOICES) + year = models.IntegerField(choices=settings.YEARS, default=1) + semester = models.CharField(choices=settings.SEMESTER_CHOICES, max_length=200) is_elective = models.BooleanField(default=False) objects = CourseManager() diff --git a/course/utils.py b/course/utils.py index 48d1108..e69de29 100644 --- a/course/utils.py +++ b/course/utils.py @@ -1,30 +0,0 @@ -import datetime -import os -import random -import string - -from django.utils.text import slugify - - -def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits): - return "".join(random.choice(chars) for _ in range(size)) - - -def unique_slug_generator(instance, new_slug=None): - """ - This is for a Django project and it assumes your instance - has a model with a slug field and a title character (char) field. - """ - if new_slug is not None: - slug = new_slug - else: - slug = slugify(instance.title) - - Klass = instance.__class__ - qs_exists = Klass.objects.filter(slug=slug).exists() - if qs_exists: - new_slug = "{slug}-{randstr}".format( - slug=slug, randstr=random_string_generator(size=4) - ) - return unique_slug_generator(instance, new_slug=new_slug) - return slug diff --git a/quiz/forms.py b/quiz/forms.py index b54448c..a2dde4a 100644 --- a/quiz/forms.py +++ b/quiz/forms.py @@ -2,11 +2,7 @@ from django.forms.widgets import RadioSelect, Textarea from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ -from django.db import transaction - from django.forms.models import inlineformset_factory - -from accounts.models import User from .models import Question, Quiz, MCQuestion, Choice diff --git a/quiz/models.py b/quiz/models.py index 97b6e57..527fe3f 100644 --- a/quiz/models.py +++ b/quiz/models.py @@ -17,7 +17,7 @@ from model_utils.managers import InheritanceManager from course.models import Course -from .utils import unique_slug_generator +from core.utils import unique_slug_generator CHOICE_ORDER_OPTIONS = ( ("content", _("Content")), diff --git a/quiz/utils.py b/quiz/utils.py index 48d1108..e69de29 100644 --- a/quiz/utils.py +++ b/quiz/utils.py @@ -1,30 +0,0 @@ -import datetime -import os -import random -import string - -from django.utils.text import slugify - - -def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits): - return "".join(random.choice(chars) for _ in range(size)) - - -def unique_slug_generator(instance, new_slug=None): - """ - This is for a Django project and it assumes your instance - has a model with a slug field and a title character (char) field. - """ - if new_slug is not None: - slug = new_slug - else: - slug = slugify(instance.title) - - Klass = instance.__class__ - qs_exists = Klass.objects.filter(slug=slug).exists() - if qs_exists: - new_slug = "{slug}-{randstr}".format( - slug=slug, randstr=random_string_generator(size=4) - ) - return unique_slug_generator(instance, new_slug=new_slug) - return slug diff --git a/result/models.py b/result/models.py index d60a546..a24dfc0 100644 --- a/result/models.py +++ b/result/models.py @@ -1,4 +1,5 @@ from decimal import Decimal +from django.conf import settings from django.db import models from django.urls import reverse @@ -7,34 +8,6 @@ from core.models import Semester from course.models import Course -# Constants -YEARS = ( - (1, "1"), - (2, "2"), - (3, "3"), - (4, "4"), - (5, "5"), - (6, "6"), -) - -BACHELOR_DEGREE = "Bachelor" -MASTER_DEGREE = "Master" - -LEVEL_CHOICES = ( - (BACHELOR_DEGREE, "Bachelor Degree"), - (MASTER_DEGREE, "Master Degree"), -) - -FIRST = "First" -SECOND = "Second" -THIRD = "Third" - -SEMESTER_CHOICES = ( - (FIRST, "First"), - (SECOND, "Second"), - (THIRD, "Third"), -) - A_PLUS = "A+" A = "A" A_MINUS = "A-" @@ -208,9 +181,9 @@ class Result(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE) gpa = models.FloatField(null=True) cgpa = models.FloatField(null=True) - semester = models.CharField(max_length=100, choices=SEMESTER_CHOICES) + semester = models.CharField(max_length=100, choices=settings.SEMESTER_CHOICES) session = models.CharField(max_length=100, blank=True, null=True) - level = models.CharField(max_length=25, choices=LEVEL_CHOICES, null=True) + level = models.CharField(max_length=25, choices=settings.LEVEL_CHOICES, null=True) def __str__(self): return f"Result for {self.student} - Semester: {self.semester}, Level: {self.level}" From 1055e12273d2e6e0a50e4492d8865a094a7da77f Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 02:36:33 +0300 Subject: [PATCH 08/17] ignore import-error for now --- .pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index c27897b..567845c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -452,7 +452,8 @@ disable=raw-checker-failed, invalid-overridden-method, unsupported-binary-operation, attribute-defined-outside-init, - duplicate-code + duplicate-code, + import-error # 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 From efdd08fe58558288a86e2f8d28ec56de4c425c7d Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 17:57:45 +0300 Subject: [PATCH 09/17] fix: search result cards --- result/views.py | 6 +++--- templates/search/search_view.html | 18 ++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/result/views.py b/result/views.py index edb29af..e948061 100644 --- a/result/views.py +++ b/result/views.py @@ -25,7 +25,7 @@ from core.models import Session, Semester from course.models import Course from accounts.decorators import lecturer_required, student_required -from .models import TakenCourse, Result, FIRST, SECOND +from .models import TakenCourse, Result cm = 2.54 @@ -583,7 +583,7 @@ def course_registration_form(request): first_semester_unit = 0 for course in courses: - if course.course.semester == FIRST: + if course.course.semester == settings.FIRST: first_semester_unit += int(course.course.credit) data = [ ( @@ -672,7 +672,7 @@ def course_registration_form(request): second_semester_unit = 0 for course in courses: - if course.course.semester == SECOND: + if course.course.semester == settings.SECOND: second_semester_unit += int(course.course.credit) data = [ ( diff --git a/templates/search/search_view.html b/templates/search/search_view.html index a8885d6..271ca8b 100644 --- a/templates/search/search_view.html +++ b/templates/search/search_view.html @@ -30,12 +30,6 @@ .class-item a{padding: 2px; color: #343a40; text-decoration: none; transition: .5s;} .class-item:hover{ transform: translateX(15px); - background: #6cbd45; - } - .class-item:hover h4 a { - color: #fff; } - .class-item:hover p, .class-item:hover span { - color: rgb(158, 239, 119); } @@ -46,7 +40,7 @@
{{ count }} {% trans 'result' %}{{ count|pluralize }} {% with object|class_name as klass %} {% if klass == "Program" %}
-
{% trans 'Program' %}
+
{% trans 'Program' %}
@@ -56,7 +50,7 @@

{{ object.title}}

{% elif klass == "Course" %}
-
{% trans 'Course' %}
+
{% trans 'Course' %}

{% trans 'Program of' %} {{ object.program }}

@@ -66,7 +60,7 @@

{{ object }}

{% elif klass == "NewsAndEvents" %}
-
{% trans 'News And Events' %}
+
{% trans 'News And Events' %}

{% trans 'Date:' %} {{ object.updated_date|timesince }} ago

@@ -76,7 +70,7 @@

{{ object.title }}

{% elif klass == "Quiz" %}
-
{% trans 'Quiz' %}
+
{% trans 'Quiz' %}

{{ object.category }} {% trans 'quiz' %}, {% trans 'Course:' %} {{ object.course }}

@@ -86,7 +80,7 @@

{{ object.title }}

{% else %}
-
{% trans 'Program' %}
+
{% trans 'Program' %}
{{ object }} | {{ object|class_name }} @@ -123,7 +117,7 @@
{% trans 'Search by:' %}
  • {% trans 'Program' %} > {% trans 'Title or Description' %}
  • {% trans 'Course' %} >{% trans 'Title, Code or Description' %}
  • -
  • {% trans 'News And Events' %} > {% trans 'Title, Description or just by typing "news" or "event %}li> +
  • {% trans 'News And Events' %} > {% trans 'Title, Description or just by typing news or event' %}li>
  • {% trans 'Quiz' %} >{% trans 'Title, Description or Category(practice, assignment and exam)' %}
From 7c7376c7ab951a46fccd636ed7ce917ef38f1571 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 18:28:02 +0300 Subject: [PATCH 10/17] Move all styles to style.scss --- static/css/style.min.css | 2 +- static/css/style.min.css.map | 2 +- static/scss/style.scss | 167 ++++++++++++++++++ templates/base.html | 2 +- templates/core/index.html | 24 --- templates/pdf/lecturer_list.html | 31 ---- templates/pdf/profile_single.html | 27 --- templates/pdf/student_list.html | 31 ---- templates/quiz/progress.html | 8 +- templates/quiz/quiz_list.html | 1 - ...g_detail.html => quiz_marking_detail.html} | 4 +- templates/quiz/result.html | 2 - templates/quiz/sitting_list.html | 3 - templates/search/search_view.html | 20 --- templates/{aside.html => sidebar.html} | 7 - templates/upload/video_single.html | 8 - 16 files changed, 179 insertions(+), 160 deletions(-) rename templates/quiz/{sitting_detail.html => quiz_marking_detail.html} (92%) rename templates/{aside.html => sidebar.html} (98%) diff --git a/static/css/style.min.css b/static/css/style.min.css index f42f4f0..674f239 100644 --- a/static/css/style.min.css +++ b/static/css/style.min.css @@ -1 +1 @@ -@font-face{font-family:"Rubik";font-style:normal;font-weight:300;src:url("../fonts/rubik-v14-latin/rubik-v14-latin-300.eot");src:local(""),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.eot?#iefix") format("embedded-opentype"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.woff2") format("woff2"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.woff") format("woff"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.ttf") format("truetype"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.svg#Rubik") format("svg")}*,body{font-family:"Rubik",sans-serif}body{background-color:#f3f2f2}::-webkit-scrollbar{width:.8vw;height:.8vw}::-webkit-scrollbar-track{background:#f3f2f2;border-radius:.5vw}::-webkit-scrollbar-thumb{background:#ccc;border-radius:.5vw}::-webkit-scrollbar-thumb:hover{background:#999}.dim{box-shadow:0 0 0 10000px rgba(0,0,0,.5) !important;box-shadow:0 0 0 100vmax rgba(0,0,0,.5) !important}.table td,.table th{vertical-align:middle}.table tbody>tr>td>a{display:flex;color:#2196f3;padding:.5rem 1rem;transition:.2s}.table tbody>tr>td>a:hover{background-color:rgba(157,220,223,.3);border-radius:.2em}.table tbody>tr>td>a:focus{box-shadow:0 0 0 .3rem rgba(127,190,193,.5)}.table input{padding:10px;max-width:130px;border-style:none;border:1px solid #bbb;border-radius:.25rem;transition:.3s}.table input:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.table .dropdown #dropdownMenuButton{color:#999}.table-title{text-transform:uppercase;font-size:16px;padding:10px;margin:10px 0;color:#2196f3}#main{padding-top:65px;padding-bottom:3rem;padding-left:300px;transition:.5s}@media(max-width: 800px){#main{padding-top:115px}}#top-navbar{position:fixed;top:0;right:0;left:300px;-webkit-margin-start:-10px;z-index:90;background:#f5f5f5;box-shadow:0px 2px 5px 0px rgba(0,0,0,.1);transition:.3s}#top-navbar .nav-wrapper{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}#top-navbar .nav-wrapper .form-header{display:flex;flex:.8}#top-navbar .nav-wrapper .form-header .au-input{flex:.9}#top-navbar .nav-wrapper .form-header button{flex:.1}#top-navbar .nav-wrapper .toggle-btn{cursor:pointer;padding:.2rem .5rem}#top-navbar .nav-wrapper .toggle-btn:hover{background-color:#fff}#top-navbar.toggle-active{left:0}@media(max-width: 800px){#top-navbar .nav-wrapper .form-header{order:2}#top-navbar .nav-wrapper .toggle-btn{order:1}#top-navbar .nav-wrapper .dropdown{order:3}}.manage-wrap{position:fixed;bottom:0;right:0;left:300px;padding:.5rem;z-index:10;background-color:rgba(255,255,255,.8);border-top:1px solid #6c757d;transition:.3s}.manage-wrap.toggle-active{left:0}.au-input{display:flex;width:auto;line-height:40px;border:1px solid #e5e5e5;font-family:inherit;font-size:13px;color:#666;padding:0 17px;border-radius:3px;transition:all .2s ease}.au-input:focus{border:1px solid #343a40}.au-input--xl{min-width:935px}@media(max-width: 1600px){.au-input--xl{min-width:500px}}@media(max-width: 1000px){.au-input--xl{min-width:150px}}@media(max-width: 767px){.au-input--xl{min-width:150px;max-height:45px}}@media(max-width: 800px){.nav-wrapper .form-header{order:1;width:100%}.nav-wrapper .form-header .au-input--xl{width:100%}.nav-wrapper .toggle-btn{order:2}.nav-wrapper .dropdown{order:3}}.avatar{width:40px;height:40px;border-radius:50%;overflow:hidden}.avatar img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.avatar img:hover{filter:contrast(0.9)}.avatar.avatar-md{width:60px;height:60px}.avatar.avatar-lg{width:80px;height:80px}.dropdown-menu{box-shadow:0 0 15px 0 rgba(0,0,0,.3)}.dropdown-menu .dropdown-item{padding-top:8px;padding-bottom:8px}@keyframes grow-top{0%{transform:scale(0.8)}100%{transform:scale(1)}}#side-nav{width:300px;position:fixed;left:0;top:0;bottom:0;display:flex;flex-direction:column;justify-content:space-between;z-index:100;overflow-y:auto;resize:horizontal;background-color:#fff;box-shadow:0px 2px 5px 0px rgba(0,0,0,.16),0px 2px 10px 0px rgba(0,0,0,.12);transition:.3s}#side-nav i{margin-right:8px}#side-nav footer{margin-top:4rem}#side-nav .top-side{background:#f5f5f5;box-shadow:0px 2px 5px 0px rgba(0,0,0,.1);margin-bottom:10px;padding:.5rem 2rem}#side-nav .top-side .desktop-hide{display:none}#side-nav .top-side .desktop-hide .toggle-btn{position:absolute;top:0;right:0;left:0;background-color:#fff;color:#f2f2f2;padding:0 1rem;border-radius:2px;cursor:pointer;margin-left:auto;transition:.5s}#side-nav .top-side .desktop-hide .toggle-btn i{color:#999;margin:0 auto}#side-nav .top-side .desktop-hide .toggle-btn i:hover{color:#666;transition:.2s}#side-nav .top-side .logo img{width:90%}#side-nav ul{padding:0}#side-nav ul li{list-style:none}#side-nav ul li:last-child{border-bottom:none}#side-nav ul li a{display:flex;align-items:center;padding:.8rem 1rem;color:#666;border-radius:0 2em 2em 0;transition:.25s}#side-nav ul li a:hover{color:#007bff;background:rgba(0,111,255,.1)}#side-nav ul li.active a{background:var(--bs-primary);color:#fff}#side-nav.toggle-active{box-shadow:0px 0px 0px 0px #dbdbdb;left:-300px}@media screen and (max-width: 1150px){#side-nav .top-side{padding-top:3rem}#side-nav .top-side .desktop-hide{display:block}}#main.toggle-active{box-shadow:0px 0px 0px 0px #dbdbdb;padding-left:0px}@media screen and (max-width: 1150px){#side-nav{left:-300px}#side-nav.toggle-active{left:0;box-shadow:0 0 0 10000px rgba(0,0,0,.5)}#main{padding-left:0}#top-navbar{left:0}.manage-wrap{left:0}}#input-nav{display:flex;flex-wrap:wrap;align-items:center;padding:.3rem .9rem;margin-bottom:1rem;color:#fd7e14;border-radius:3px;background-color:#fff !important;box-shadow:inset 0 0 2.5rem rgba(0,0,0,.2)}#input-nav a{color:#007bff}#input-nav a:hover{text-decoration:underline}#input-nav a::after{content:">";color:#666;margin:0 5px;vertical-align:middle}.footer{top:100%;bottom:0;display:block;text-align:center;justify-content:center;padding:.75rem 1.25rem;margin-top:4rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.1)}.footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}a{color:var(--bs-primary);text-decoration:none}.title-1{position:relative;display:inline-flex;align-items:center;font-family:inherit;text-transform:capitalize;font-weight:700;font-size:24px;border-radius:.2em}.title-1::before{content:"";position:absolute;bottom:0;right:-5px;width:50%;height:15px;z-index:-1;border-radius:1px;background-color:rgba(83,126,226,.2);animation:lineAnim 1s ease-in forwards}.title-1 i{margin-right:8px}@keyframes lineAnim{0%{width:0px;height:4px}60%{width:50%;height:4px}100%{width:50%;height:15px}}.form-title{display:flex;align-items:center;justify-content:center;font-weight:400;font-size:17px;padding:10px;background:linear-gradient(40deg, #45cafc, #303f9f);color:#fff;text-align:center;margin-bottom:10px}.form-title i{margin-right:8px}.news{background:linear-gradient(40deg, #45cafc, #303f9f) !important;color:#fff}.events{background:linear-gradient(40deg, #ad86f6, #572ca7) !important;color:#fff}.allocate-btn{text-align:center;width:auto;padding:10px 20px;cursor:pointer;color:#5c6ac4;border:1px solid #ddd;border-radius:2rem;transition:.2s}.allocate-btn:hover{background:#ddd}.link{color:#2196f3;margin-top:40px;transition:.2s}.link:hover{color:#0b21ad}.score-wrapper{position:relative;display:flex;width:7rem;height:7rem;overflow:hidden;font-size:.75rem;background-color:#d5dce4;border-radius:50%;box-shadow:0px 0px 3px 10px #f0f0f0}.score-wrapper>.score-wrapper-bar{position:absolute;bottom:0;width:100%;transition:width 6s ease}.score-wrapper>.score-wrapper-text{position:absolute;font-size:20px;height:100%;width:100%;z-index:1;color:#fff;display:flex;justify-content:center;align-items:center}.score-wrapper .bg-success{background-color:#6cbd45 !important}.score-wrapper .bg-warning{background-color:#ffc107 !important}.score-wrapper .bg-danger{background-color:#f53d3d !important}.bg-sub-info{background-color:#35b6cc !important;color:#fff}.main-progress{animation:main-progress1 7s ease-in-out forwards}@keyframes main-progress1{0%{transform:scale(0)}95%{transform:scale(0)}100%{transform:scale(1)}}#progress-card{display:flex;align-items:center;justify-content:center;position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;overflow:hidden;background:#fff}.loader{position:relative;display:flex;justify-content:center;align-items:center;line-height:5.6;animation:loader-in-out 7s ease-in-out forwards}@keyframes loader-in-out{0%{transform:scale(0)}10%{transform:scale(1)}95%{transform:scale(1)}100%{transform:scale(0)}}.progress-bar{animation:loader-bar ease-in-out 7s forwards}@keyframes loader-bar{0%,10%{width:0%}50%,70%{width:50%}80%,95%{width:97%}100%{width:100%}}@media screen and (max-width: 500px){.content-center{display:block}.mobile-hide-500{display:none}.save-btn{font-size:14px}.title-1{font-size:20px}}@media screen and (max-width: 450px){.mobile-hide-450{display:none}}.edit-btn i{margin-right:10px}@media screen and (max-width: 450px){.edit-btn i{margin-right:0}}.switch.switch-text{position:relative;display:inline-block;vertical-align:top;width:48px;height:24px;background-color:rgba(0,0,0,0);cursor:pointer}.switch.switch-text .switch-input{position:absolute;top:0;left:0;opacity:0}.switch.switch-text .switch-label{position:relative;display:block;height:inherit;font-size:10px;font-weight:600;text-transform:uppercase;background-color:#dc3545;border-radius:2px;transition:opacity background-color .15s ease-out}.switch.switch-text .switch-label::before,.switch.switch-text .switch-label::after{position:absolute;top:50%;width:50%;margin-top:-0.5em;line-height:1;text-align:center;transition:inherit}.switch.switch-text .switch-label::before{right:1px;color:#e9ecef;content:attr(data-off)}.switch.switch-text .switch-label::after{left:1px;color:#fff;content:attr(data-on);opacity:0}.switch.switch-text .switch-input:checked~.switch-label::before{opacity:0}.switch.switch-text .switch-input:checked~.switch-label::after{opacity:1}.switch.switch-text .switch-handle{position:absolute;top:2px;left:2px;width:20px;height:20px;background:#fff;border-color:#fff;border-radius:1px;transition:left .15s ease-out}.switch.switch-text .switch-input:checked~.switch-handle{left:26px}.switch.switch-text.switch-lg{width:56px;height:28px}.switch.switch-text.switch-lg .switch-label{font-size:12px}.switch.switch-text.switch-lg .switch-handle{width:24px;height:24px}.switch.switch-text.switch-lg .switch-input:checked~.switch-handle{left:30px}.switch.switch-text.switch-sm{width:40px;height:20px}.switch.switch-text.switch-sm .switch-label{font-size:8px}.switch.switch-text.switch-sm .switch-handle{width:16px;height:16px}.switch.switch-text.switch-sm .switch-input:checked~.switch-handle{left:22px}.switch.switch-text.switch-xs{width:32px;height:16px}.switch.switch-text.switch-xs .switch-label{font-size:7px}.switch.switch-text.switch-xs .switch-handle{width:12px;height:12px}.switch.switch-text.switch-xs .switch-input:checked~.switch-handle{left:18px}.switch-pill .switch-label,.switch.switch-3d .switch-label,.switch-pill .switch-handle,.switch.switch-3d .switch-handle{border-radius:50em !important}.switch-pill .switch-label::before,.switch.switch-3d .switch-label::before{right:2px !important}.switch-pill .switch-label::after,.switch.switch-3d .switch-label::after{left:2px !important}.switch-success>.switch-input:checked~.switch-label{background:#28a745 !important;border-color:#1e7e34}.switch-success>.switch-input:checked~.switch-handle{border-color:#1e7e34}.switch-success-outline>.switch-input:checked~.switch-label{background:#fff !important;border-color:#28a745}.switch-success-outline>.switch-input:checked~.switch-label::after{color:#28a745}.switch-success-outline>.switch-input:checked~.switch-handle{border-color:#28a745}.switch-success-outline-alt>.switch-input:checked~.switch-label{background:#fff !important;border-color:#28a745}.switch-success-outline-alt>.switch-input:checked~.switch-label::after{color:#28a745}.switch-success-outline-alt>.switch-input:checked~.switch-handle{background:#28a745 !important;border-color:#28a745}.fas,.fa{font-size:16px;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;background-color:rgba(0,0,0,.093);border-radius:50%}.fas.unstyled,.fa.unstyled{background-color:unset;border-radius:unset}.card-count .fas,.card-count .fa{font-size:24px;display:initial;align-items:initial;justify-content:initial;width:initial;height:initial;background-color:initial;border-radius:initial}.card-count{display:flex;flex-direction:column;align-items:center;justify-content:center}.card-count .text-right{display:flex;flex-direction:row-reverse;align-items:flex-end;gap:5px}.card-count .text-right h2{margin:0}@media(min-width: 800px){.card-count{display:flex;flex-direction:row;align-items:center;justify-content:space-between}.card-count .text-right{display:block;text-align:end}.card-count .text-right h2{margin:0}.card-count .fas,.card-count .fa{border-right:1px solid #ddd}.users-count .card-count h3{border-right:1px solid #e6e6e6}}.chart-wrap{position:relative;padding:1rem;transition:.5s;background-color:#fff;border-radius:10px}.fa-expand-alt{display:none;position:absolute;top:.5rem;right:.5rem;padding:.5rem;cursor:pointer;transition:.3s}.fa-expand-alt:hover{background-color:#f1f1f1}.chart-wrap:hover{box-shadow:0 0 0 1px inset #666}.chart-wrap:hover .fa-expand-alt{display:block}.expand{transform:translateY(100%);position:fixed;bottom:0;top:3rem;left:0;right:0;width:100%;z-index:999;flex:0 0 100%;background-color:#fff;box-shadow:0 0 0 10000px rgba(0,0,0,.5) !important;box-shadow:0 0 0 100vmax rgba(0,0,0,.5) !important;transform-origin:bottom left;animation:popupAnim forwards alternate .5s ease-in-out;overflow:auto}.expand .fa-expand-alt{display:block}@keyframes popupAnim{from{transform:translateY(100%)}to{transform:translateY(0)}}.users-count .card-count{width:100%;height:100%;display:flex;justify-content:space-between;align-items:center;background-color:#fff}.users-count .card-count h2{font-weight:1000}.users-count .card-count h3{flex:0 0 40%}.users-count .card-count h3 i{display:inline-flex;width:60px;height:60px;display:flex;align-items:center;justify-content:center;border-radius:50%}.bg-light-aqua{background-color:rgba(32,177,177,.8) !important;box-shadow:0 0 0 10px rgba(32,177,177,.2) !important;color:#fff !important}.bg-light-orange{background-color:rgba(253,174,28,.8) !important;box-shadow:0 0 0 10px rgba(253,174,28,.2) !important;color:#fff !important}.bg-light-purple{background-color:rgba(203,31,255,.8) !important;box-shadow:0 0 0 10px rgba(203,31,255,.2) !important;color:#fff !important}.bg-light-red{background-color:rgba(255,19,157,.8) !important;box-shadow:0 0 0 10px rgba(255,19,157,.2) !important;color:#fff !important}.activities ul{padding-left:.5rem}.activities ul li{list-style-type:disc}/*# sourceMappingURL=style.min.css.map */ \ No newline at end of file +@font-face{font-family:"Rubik";font-style:normal;font-weight:300;src:url("../fonts/rubik-v14-latin/rubik-v14-latin-300.eot");src:local(""),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.eot?#iefix") format("embedded-opentype"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.woff2") format("woff2"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.woff") format("woff"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.ttf") format("truetype"),url("../fonts/rubik-v14-latin/rubik-v14-latin-300.svg#Rubik") format("svg")}*,body{font-family:"Rubik",sans-serif}body{background-color:#f3f2f2}::-webkit-scrollbar{width:.8vw;height:.8vw}::-webkit-scrollbar-track{background:#f3f2f2;border-radius:.5vw}::-webkit-scrollbar-thumb{background:#ccc;border-radius:.5vw}::-webkit-scrollbar-thumb:hover{background:#999}.dim{box-shadow:0 0 0 10000px rgba(0,0,0,.5) !important;box-shadow:0 0 0 100vmax rgba(0,0,0,.5) !important}.table td,.table th{vertical-align:middle}.table tbody>tr>td>a{display:flex;color:#2196f3;padding:.5rem 1rem;transition:.2s}.table tbody>tr>td>a:hover{background-color:rgba(157,220,223,.3);border-radius:.2em}.table tbody>tr>td>a:focus{box-shadow:0 0 0 .3rem rgba(127,190,193,.5)}.table input{padding:10px;max-width:130px;border-style:none;border:1px solid #bbb;border-radius:.25rem;transition:.3s}.table input:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.table .dropdown #dropdownMenuButton{color:#999}.table-title{text-transform:uppercase;font-size:16px;padding:10px;margin:10px 0;color:#2196f3}#main{padding-top:65px;padding-bottom:3rem;padding-left:300px;transition:.5s}@media(max-width: 800px){#main{padding-top:115px}}#top-navbar{position:fixed;top:0;right:0;left:300px;-webkit-margin-start:-10px;z-index:90;background:#f5f5f5;box-shadow:0px 2px 5px 0px rgba(0,0,0,.1);transition:.3s}#top-navbar .nav-wrapper{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}#top-navbar .nav-wrapper .form-header{display:flex;flex:.8}#top-navbar .nav-wrapper .form-header .au-input{flex:.9}#top-navbar .nav-wrapper .form-header button{flex:.1}#top-navbar .nav-wrapper .toggle-btn{cursor:pointer;padding:.2rem .5rem}#top-navbar .nav-wrapper .toggle-btn:hover{background-color:#fff}#top-navbar.toggle-active{left:0}@media(max-width: 800px){#top-navbar .nav-wrapper .form-header{order:2}#top-navbar .nav-wrapper .toggle-btn{order:1}#top-navbar .nav-wrapper .dropdown{order:3}}.manage-wrap{position:fixed;bottom:0;right:0;left:300px;padding:.5rem;z-index:10;background-color:rgba(255,255,255,.8);border-top:1px solid #6c757d;transition:.3s}.manage-wrap.toggle-active{left:0}.au-input{display:flex;width:auto;line-height:40px;border:1px solid #e5e5e5;font-family:inherit;font-size:13px;color:#666;padding:0 17px;border-radius:3px;transition:all .2s ease}.au-input:focus{border:1px solid #343a40}.au-input--xl{min-width:935px}@media(max-width: 1600px){.au-input--xl{min-width:500px}}@media(max-width: 1000px){.au-input--xl{min-width:150px}}@media(max-width: 767px){.au-input--xl{min-width:150px;max-height:45px}}@media(max-width: 800px){.nav-wrapper .form-header{order:1;width:100%}.nav-wrapper .form-header .au-input--xl{width:100%}.nav-wrapper .toggle-btn{order:2}.nav-wrapper .dropdown{order:3}}.avatar{width:40px;height:40px;border-radius:50%;overflow:hidden}.avatar img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.avatar img:hover{filter:contrast(0.9)}.avatar.avatar-md{width:60px;height:60px}.avatar.avatar-lg{width:80px;height:80px}.dropdown-menu{box-shadow:0 0 15px 0 rgba(0,0,0,.3)}.dropdown-menu .dropdown-item{padding-top:8px;padding-bottom:8px}@keyframes grow-top{0%{transform:scale(0.8)}100%{transform:scale(1)}}#side-nav{width:300px;position:fixed;left:0;top:0;bottom:0;display:flex;flex-direction:column;justify-content:space-between;z-index:100;overflow-y:auto;resize:horizontal;background-color:#fff;box-shadow:0px 2px 5px 0px rgba(0,0,0,.16),0px 2px 10px 0px rgba(0,0,0,.12);transition:.3s}#side-nav i{margin-right:8px}#side-nav footer{margin-top:4rem}#side-nav .top-side{background:#f5f5f5;box-shadow:0px 2px 5px 0px rgba(0,0,0,.1);margin-bottom:10px;padding:.5rem 2rem}#side-nav .top-side .desktop-hide{display:none}#side-nav .top-side .desktop-hide .toggle-btn{position:absolute;top:0;right:0;left:0;background-color:#fff;color:#f2f2f2;padding:0 1rem;border-radius:2px;cursor:pointer;margin-left:auto;transition:.5s}#side-nav .top-side .desktop-hide .toggle-btn i{color:#999;margin:0 auto}#side-nav .top-side .desktop-hide .toggle-btn i:hover{color:#666;transition:.2s}#side-nav .top-side .logo img{width:90%}#side-nav ul{padding:0}#side-nav ul li{list-style:none}#side-nav ul li:last-child{border-bottom:none}#side-nav ul li a{display:flex;align-items:center;padding:.8rem 1rem;color:#666;border-radius:0 2em 2em 0;transition:.25s}#side-nav ul li a:hover{color:#007bff;background:rgba(0,111,255,.1)}#side-nav ul li.active a{background:var(--bs-primary);color:#fff}#side-nav.toggle-active{box-shadow:0px 0px 0px 0px #dbdbdb;left:-300px}@media screen and (max-width: 1150px){#side-nav .top-side{padding-top:3rem}#side-nav .top-side .desktop-hide{display:block}}#main.toggle-active{box-shadow:0px 0px 0px 0px #dbdbdb;padding-left:0px}@media screen and (max-width: 1150px){#side-nav{left:-300px}#side-nav.toggle-active{left:0;box-shadow:0 0 0 10000px rgba(0,0,0,.5)}#main{padding-left:0}#top-navbar{left:0}.manage-wrap{left:0}}#input-nav{display:flex;flex-wrap:wrap;align-items:center;padding:.3rem .9rem;margin-bottom:1rem;color:#fd7e14;border-radius:3px;background-color:#fff !important;box-shadow:inset 0 0 2.5rem rgba(0,0,0,.2)}#input-nav a{color:#007bff}#input-nav a:hover{text-decoration:underline}#input-nav a::after{content:">";color:#666;margin:0 5px;vertical-align:middle}.footer{top:100%;bottom:0;display:block;text-align:center;justify-content:center;padding:.75rem 1.25rem;margin-top:4rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.1)}.footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}a{color:var(--bs-primary);text-decoration:none}.title-1{position:relative;display:inline-flex;align-items:center;font-family:inherit;text-transform:capitalize;font-weight:700;font-size:24px;border-radius:.2em}.title-1::before{content:"";position:absolute;bottom:0;right:-5px;width:50%;height:15px;z-index:-1;border-radius:1px;background-color:rgba(83,126,226,.2);animation:lineAnim 1s ease-in forwards}.title-1 i{margin-right:8px}@keyframes lineAnim{0%{width:0px;height:4px}60%{width:50%;height:4px}100%{width:50%;height:15px}}.form-title{display:flex;align-items:center;justify-content:center;font-weight:400;font-size:17px;padding:10px;background:linear-gradient(40deg, #45cafc, #303f9f);color:#fff;text-align:center;margin-bottom:10px}.form-title i{margin-right:8px}.news{background:linear-gradient(40deg, #45cafc, #303f9f) !important;color:#fff}.events{background:linear-gradient(40deg, #ad86f6, #572ca7) !important;color:#fff}.allocate-btn{text-align:center;width:auto;padding:10px 20px;cursor:pointer;color:#5c6ac4;border:1px solid #ddd;border-radius:2rem;transition:.2s}.allocate-btn:hover{background:#ddd}.link{color:#2196f3;margin-top:40px;transition:.2s}.link:hover{color:#0b21ad}.score-wrapper{position:relative;display:flex;width:7rem;height:7rem;overflow:hidden;font-size:.75rem;background-color:#d5dce4;border-radius:50%;box-shadow:0px 0px 3px 10px #f0f0f0}.score-wrapper>.score-wrapper-bar{position:absolute;bottom:0;width:100%;transition:width 6s ease}.score-wrapper>.score-wrapper-text{position:absolute;font-size:20px;height:100%;width:100%;z-index:1;color:#fff;display:flex;justify-content:center;align-items:center}.score-wrapper .bg-success{background-color:#6cbd45 !important}.score-wrapper .bg-warning{background-color:#ffc107 !important}.score-wrapper .bg-danger{background-color:#f53d3d !important}.bg-sub-info{background-color:#35b6cc !important;color:#fff}.main-progress{animation:main-progress1 7s ease-in-out forwards}@keyframes main-progress1{0%{transform:scale(0)}95%{transform:scale(0)}100%{transform:scale(1)}}#progress-card{display:flex;align-items:center;justify-content:center;position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;overflow:hidden;background:#fff}.loader{position:relative;display:flex;justify-content:center;align-items:center;line-height:5.6;animation:loader-in-out 7s ease-in-out forwards}@keyframes loader-in-out{0%{transform:scale(0)}10%{transform:scale(1)}95%{transform:scale(1)}100%{transform:scale(0)}}.progress-bar{animation:loader-bar ease-in-out 7s forwards}@keyframes loader-bar{0%,10%{width:0%}50%,70%{width:50%}80%,95%{width:97%}100%{width:100%}}@media screen and (max-width: 500px){.content-center{display:block}.mobile-hide-500{display:none}.save-btn{font-size:14px}.title-1{font-size:20px}}@media screen and (max-width: 450px){.mobile-hide-450{display:none}}.edit-btn i{margin-right:10px}@media screen and (max-width: 450px){.edit-btn i{margin-right:0}}.switch.switch-text{position:relative;display:inline-block;vertical-align:top;width:48px;height:24px;background-color:rgba(0,0,0,0);cursor:pointer}.switch.switch-text .switch-input{position:absolute;top:0;left:0;opacity:0}.switch.switch-text .switch-label{position:relative;display:block;height:inherit;font-size:10px;font-weight:600;text-transform:uppercase;background-color:#dc3545;border-radius:2px;transition:opacity background-color .15s ease-out}.switch.switch-text .switch-label::before,.switch.switch-text .switch-label::after{position:absolute;top:50%;width:50%;margin-top:-0.5em;line-height:1;text-align:center;transition:inherit}.switch.switch-text .switch-label::before{right:1px;color:#e9ecef;content:attr(data-off)}.switch.switch-text .switch-label::after{left:1px;color:#fff;content:attr(data-on);opacity:0}.switch.switch-text .switch-input:checked~.switch-label::before{opacity:0}.switch.switch-text .switch-input:checked~.switch-label::after{opacity:1}.switch.switch-text .switch-handle{position:absolute;top:2px;left:2px;width:20px;height:20px;background:#fff;border-color:#fff;border-radius:1px;transition:left .15s ease-out}.switch.switch-text .switch-input:checked~.switch-handle{left:26px}.switch.switch-text.switch-lg{width:56px;height:28px}.switch.switch-text.switch-lg .switch-label{font-size:12px}.switch.switch-text.switch-lg .switch-handle{width:24px;height:24px}.switch.switch-text.switch-lg .switch-input:checked~.switch-handle{left:30px}.switch.switch-text.switch-sm{width:40px;height:20px}.switch.switch-text.switch-sm .switch-label{font-size:8px}.switch.switch-text.switch-sm .switch-handle{width:16px;height:16px}.switch.switch-text.switch-sm .switch-input:checked~.switch-handle{left:22px}.switch.switch-text.switch-xs{width:32px;height:16px}.switch.switch-text.switch-xs .switch-label{font-size:7px}.switch.switch-text.switch-xs .switch-handle{width:12px;height:12px}.switch.switch-text.switch-xs .switch-input:checked~.switch-handle{left:18px}.switch-pill .switch-label,.switch.switch-3d .switch-label,.switch-pill .switch-handle,.switch.switch-3d .switch-handle{border-radius:50em !important}.switch-pill .switch-label::before,.switch.switch-3d .switch-label::before{right:2px !important}.switch-pill .switch-label::after,.switch.switch-3d .switch-label::after{left:2px !important}.switch-success>.switch-input:checked~.switch-label{background:#28a745 !important;border-color:#1e7e34}.switch-success>.switch-input:checked~.switch-handle{border-color:#1e7e34}.switch-success-outline>.switch-input:checked~.switch-label{background:#fff !important;border-color:#28a745}.switch-success-outline>.switch-input:checked~.switch-label::after{color:#28a745}.switch-success-outline>.switch-input:checked~.switch-handle{border-color:#28a745}.switch-success-outline-alt>.switch-input:checked~.switch-label{background:#fff !important;border-color:#28a745}.switch-success-outline-alt>.switch-input:checked~.switch-label::after{color:#28a745}.switch-success-outline-alt>.switch-input:checked~.switch-handle{background:#28a745 !important;border-color:#28a745}.fas,.fa{font-size:16px;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;background-color:rgba(0,0,0,.093);border-radius:50%}.fas.unstyled,.fa.unstyled{background-color:unset;border-radius:unset}.card-count .fas,.card-count .fa{font-size:24px;display:initial;align-items:initial;justify-content:initial;width:initial;height:initial;background-color:initial;border-radius:initial}.card-count{display:flex;flex-direction:column;align-items:center;justify-content:center}.card-count .text-right{display:flex;flex-direction:row-reverse;align-items:flex-end;gap:5px}.card-count .text-right h2{margin:0}@media(min-width: 800px){.card-count{display:flex;flex-direction:row;align-items:center;justify-content:space-between}.card-count .text-right{display:block;text-align:end}.card-count .text-right h2{margin:0}.card-count .fas,.card-count .fa{border-right:1px solid #ddd}.users-count .card-count h3{border-right:1px solid #e6e6e6}}.chart-wrap{position:relative;padding:1rem;transition:.5s;background-color:#fff;border-radius:10px}.fa-expand-alt{display:none;position:absolute;top:.5rem;right:.5rem;padding:.5rem;cursor:pointer;transition:.3s}.fa-expand-alt:hover{background-color:#f1f1f1}.chart-wrap:hover{box-shadow:0 0 0 1px inset #666}.chart-wrap:hover .fa-expand-alt{display:block}.expand{transform:translateY(100%);position:fixed;bottom:0;top:3rem;left:0;right:0;width:100%;z-index:999;flex:0 0 100%;background-color:#fff;box-shadow:0 0 0 10000px rgba(0,0,0,.5) !important;box-shadow:0 0 0 100vmax rgba(0,0,0,.5) !important;transform-origin:bottom left;animation:popupAnim forwards alternate .5s ease-in-out;overflow:auto}.expand .fa-expand-alt{display:block}@keyframes popupAnim{from{transform:translateY(100%)}to{transform:translateY(0)}}.users-count .card-count{width:100%;height:100%;display:flex;justify-content:space-between;align-items:center;background-color:#fff}.users-count .card-count h2{font-weight:1000}.users-count .card-count h3{flex:0 0 40%}.users-count .card-count h3 i{display:inline-flex;width:60px;height:60px;display:flex;align-items:center;justify-content:center;border-radius:50%}.bg-light-aqua{background-color:rgba(32,177,177,.8) !important;box-shadow:0 0 0 10px rgba(32,177,177,.2) !important;color:#fff !important}.bg-light-orange{background-color:rgba(253,174,28,.8) !important;box-shadow:0 0 0 10px rgba(253,174,28,.2) !important;color:#fff !important}.bg-light-purple{background-color:rgba(203,31,255,.8) !important;box-shadow:0 0 0 10px rgba(203,31,255,.2) !important;color:#fff !important}.bg-light-red{background-color:rgba(255,19,157,.8) !important;box-shadow:0 0 0 10px rgba(255,19,157,.2) !important;color:#fff !important}.activities ul{padding-left:.5rem}.activities ul li{list-style-type:disc}.top-side{background-size:cover;background-position:top center}.color-indicator{display:inline-block;width:10px;height:10px;border-radius:2px}.bg-purple{background-color:#6f42c1}.card-header-ne{position:relative;display:flex;align-items:center;justify-content:space-between}.card-header-ne .title{vertical-align:middle}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:8px;border:1px solid #ddd;text-align:left}.table th{background-color:#f2f2f2}.title-1{font-size:24px;margin-bottom:16px}.text-danger{color:red}a{color:#000;text-decoration:none}.user-picture{width:100px;height:100px;border:3px solid #fff;margin-top:-50px;-o-object-fit:cover;object-fit:cover}table .info{margin-left:-240px}.dashboard-description strong{font-weight:600}.card .h5{font-size:1.25rem;color:#333;margin-top:15px;margin-bottom:15px}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:8px;border:1px solid #ddd;text-align:left}.table th{background-color:#f2f2f2}.title-1{font-size:24px;margin-bottom:16px}.text-danger{color:red}a{color:#000;text-decoration:none}.bg-light-warning{background-color:#fcd96f !important}#progress-main{display:none}.session-wrapper{position:relative}.session{position:absolute;top:-15px;right:25px;z-index:2}.br-orange{border:1px solid #fd7e14;border-radius:7px}.class-item{display:block;border-left:4px solid #6cbd45;padding:1rem !important;background:#f8f9fa;border-radius:3px;box-shadow:0px 2px 5px 0px rgba(0,0,0,.3);transition:.5s}.class-item p{padding:2px;margin:0;color:#b4b4b4;transition:.5s}.class-item a{padding:2px;color:#343a40;text-decoration:none;transition:.5s}.class-item:hover{transform:translateX(15px)}video{max-width:100%;box-shadow:0px 2px 5px 0px rgba(0,0,0,.16),0px 2px 10px 0px rgba(0,0,0,.12)}/*# sourceMappingURL=style.min.css.map */ \ No newline at end of file diff --git a/static/css/style.min.css.map b/static/css/style.min.css.map index a1409e9..54352cb 100644 --- a/static/css/style.min.css.map +++ b/static/css/style.min.css.map @@ -1 +1 @@ -{"version":3,"sources":["../scss/style.scss"],"names":[],"mappings":"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","file":"style.min.css"} \ No newline at end of file +{"version":3,"sources":["../scss/style.scss"],"names":[],"mappings":"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","file":"style.min.css"} \ No newline at end of file diff --git a/static/scss/style.scss b/static/scss/style.scss index e5ad3eb..11f017d 100644 --- a/static/scss/style.scss +++ b/static/scss/style.scss @@ -139,6 +139,7 @@ body { transition: 0.3s; .nav-wrapper { + // height: 56px; display: flex; flex-wrap: wrap; justify-content: space-between; @@ -1134,3 +1135,169 @@ a { .activities ul li { list-style-type: disc; } + +.top-side { + background-size: cover; + background-position: top center; +} + +.color-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 2px; +} + +.bg-purple { + background-color: #6f42c1; +} + +.card-header-ne { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-header-ne .title { + vertical-align: middle; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 8px; + border: 1px solid #ddd; /* Add thin borders for separation */ + text-align: left; +} + +.table th { + background-color: #f2f2f2; +} + +.title-1 { + font-size: 24px; + margin-bottom: 16px; +} + +.text-danger { + color: red; +} + +a { + color: black; + text-decoration: none; +} + +.user-picture { + width: 100px; + height: 100px; + border: 3px solid #fff; + margin-top: -50px; + object-fit: cover; +} + +table .info { + margin-left: -240px; +} + +/* Specific to the .dashboard-description class */ +.dashboard-description strong { + font-weight: 600; +} + +/* Adjustments for headers within cards */ +.card .h5 { + font-size: 1.25rem; + color: #333; + margin-top: 15px; + margin-bottom: 15px; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 8px; + border: 1px solid #ddd; /* Add thin borders for separation */ + text-align: left; +} + +.table th { + background-color: #f2f2f2; +} + +.title-1 { + font-size: 24px; + margin-bottom: 16px; +} + +.text-danger { + color: red; +} + +a { + color: black; + text-decoration: none; +} + +.bg-light-warning { + background-color: rgb(252, 217, 111) !important; +} + +#progress-main { + display: none; +} + +.session-wrapper { + position: relative; +} +.session { + position: absolute; + top: -15px; + right: 25px; + z-index: 2; +} +.br-orange { + border: 1px solid #fd7e14; + border-radius: 7px; +} +.class-item { + display: block; + border-left: 4px solid #6cbd45; + padding: 1rem !important; + background: #f8f9fa; + border-radius: 3px; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); + transition: 0.5s; +} +.class-item p { + padding: 2px; + margin: 0; + color: #b4b4b4; + transition: 0.5s; +} +.class-item a { + padding: 2px; + color: #343a40; + text-decoration: none; + transition: 0.5s; +} +.class-item:hover { + transform: translateX(15px); +} + +video { + max-width: 100%; + -webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), + 0 2px 10px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.16), + 0px 2px 10px 0px rgba(0, 0, 0, 0.12); +} diff --git a/templates/base.html b/templates/base.html index b2a842a..4e8249f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,7 +26,7 @@ {% block sidebar %} - {% include 'aside.html' %} + {% include 'sidebar.html' %} {% endblock %} {% block maincontent %} diff --git a/templates/core/index.html b/templates/core/index.html index e4fe261..f6a5f45 100644 --- a/templates/core/index.html +++ b/templates/core/index.html @@ -5,30 +5,6 @@ {% block content %} - - +

{% trans 'Quiz Progress Rec' %}

+ {% if cat_scores %}
{% trans "Question Category Scores" %}
@@ -53,7 +55,11 @@
{% trans "Previous exam papers" %}

- {% trans "Below are the results of exams that you have sat." %} + {% if request.user.is_superuser %} + {% trans "Student exam results" %} + {% else %} + {% trans "Below are the results of exams that you have sat" %} + {% endif %}

{% trans 'Total complete exams:' %} {{ exams_counter }}
diff --git a/templates/quiz/quiz_list.html b/templates/quiz/quiz_list.html index a36c0d1..80cca1b 100644 --- a/templates/quiz/quiz_list.html +++ b/templates/quiz/quiz_list.html @@ -5,7 +5,6 @@ {% load static %} {% block content %} - -
+
{{ sitting.quiz }}
{% trans "Quiz title" %}: {{ sitting.quiz.title }}
{% trans "Category" %}: {{ sitting.quiz.category }}
@@ -59,7 +59,7 @@
{% csrf_token %} - +
diff --git a/templates/quiz/result.html b/templates/quiz/result.html index 68e0b9d..69b4541 100644 --- a/templates/quiz/result.html +++ b/templates/quiz/result.html @@ -30,8 +30,6 @@
{% trans 'Calculating your result...' %}
- -
{% if previous.answers %}
diff --git a/templates/quiz/sitting_list.html b/templates/quiz/sitting_list.html index a0ba601..558536a 100644 --- a/templates/quiz/sitting_list.html +++ b/templates/quiz/sitting_list.html @@ -11,8 +11,6 @@ -
-
{% trans "List of complete exams" %}
{% for student in students %}

{{ student.student.user.get_full_name }}

{% endfor %} @@ -62,5 +60,4 @@ {% else %}

{% trans "No completed exams for you" %}.

{% endif %} -
{% endblock %} diff --git a/templates/search/search_view.html b/templates/search/search_view.html index 271ca8b..1c58d2d 100644 --- a/templates/search/search_view.html +++ b/templates/search/search_view.html @@ -13,26 +13,6 @@ - -
{{ count }} {% trans 'result' %}{{ count|pluralize }} {% trans 'for' %} {{ query }}

diff --git a/templates/aside.html b/templates/sidebar.html similarity index 98% rename from templates/aside.html rename to templates/sidebar.html index 18504a7..e8df7ae 100644 --- a/templates/aside.html +++ b/templates/sidebar.html @@ -1,13 +1,6 @@ {% load static %} {% load i18n %} - -