Merge pull request #4 from SkyCascade/fix/pylint
Code format and best practices
This commit is contained in:
commit
9cf456ddc1
666
.pylintrc
Normal file
666
.pylintrc
Normal file
@ -0,0 +1,666 @@
|
||||
[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*(# )?<?https?://\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,
|
||||
attribute-defined-outside-init,
|
||||
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
|
||||
# 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,tests.py,tests,apps.py
|
||||
@ -1,5 +1,3 @@
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
@ -21,9 +19,8 @@ def admin_required(
|
||||
if test_func(request.user):
|
||||
# Call the original function if the user passes the test
|
||||
return function(request, *args, **kwargs) if function else None
|
||||
else:
|
||||
# Redirect to the specified URL if the user fails the test
|
||||
return redirect(redirect_to)
|
||||
# Redirect to the specified URL if the user fails the test
|
||||
return redirect(redirect_to)
|
||||
|
||||
return wrapper if function else test_func
|
||||
|
||||
@ -46,9 +43,8 @@ def lecturer_required(
|
||||
if test_func(request.user):
|
||||
# Call the original function if the user passes the test
|
||||
return function(request, *args, **kwargs) if function else None
|
||||
else:
|
||||
# Redirect to the specified URL if the user fails the test
|
||||
return redirect(redirect_to)
|
||||
# Redirect to the specified URL if the user fails the test
|
||||
return redirect(redirect_to)
|
||||
|
||||
return wrapper if function else test_func
|
||||
|
||||
@ -71,8 +67,7 @@ def student_required(
|
||||
if test_func(request.user):
|
||||
# Call the original function if the user passes the test
|
||||
return function(request, *args, **kwargs) if function else None
|
||||
else:
|
||||
# Redirect to the specified URL if the user fails the test
|
||||
return redirect(redirect_to)
|
||||
# Redirect to the specified URL if the user fails the test
|
||||
return redirect(redirect_to)
|
||||
|
||||
return wrapper if function else test_func
|
||||
|
||||
@ -44,6 +44,15 @@ class StaffAddForm(UserCreationForm):
|
||||
label="Last Name",
|
||||
)
|
||||
|
||||
gender = forms.CharField(
|
||||
widget=forms.Select(
|
||||
choices=GENDERS,
|
||||
attrs={
|
||||
"class": "browser-default custom-select form-control",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
address = forms.CharField(
|
||||
max_length=30,
|
||||
widget=forms.TextInput(
|
||||
|
||||
@ -118,7 +118,7 @@ class User(AbstractUser):
|
||||
return no_picture
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("profile_single", kwargs={"id": self.id})
|
||||
return reverse("profile_single", kwargs={"user_id": self.id})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
@ -170,7 +170,7 @@ class Student(models.Model):
|
||||
return {"M": males_count, "F": females_count}
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("profile_single", kwargs={"id": self.id})
|
||||
return reverse("profile_single", kwargs={"user_id": self.id})
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.student.delete()
|
||||
|
||||
@ -5,7 +5,7 @@ from .utils import (
|
||||
)
|
||||
|
||||
|
||||
def post_save_account_receiver(sender, instance=None, created=False, *args, **kwargs):
|
||||
def post_save_account_receiver(instance=None, created=False, *args, **kwargs):
|
||||
"""
|
||||
Send email notification
|
||||
"""
|
||||
|
||||
@ -5,38 +5,37 @@ from accounts.decorators import admin_required
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class AdminRequiredDecoratorTests(TestCase):
|
||||
def setUp(self):
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username='admin', email='admin@example.com', password='password'
|
||||
username="admin", email="admin@example.com", password="password"
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username='user', email='user@example.com', password='password'
|
||||
username="user", email="user@example.com", password="password"
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
def admin_view(self, request):
|
||||
return HttpResponse()
|
||||
|
||||
def test_admin_required_decorator(self):
|
||||
# Apply the admin_required decorator to the view function
|
||||
decorated_view = admin_required(self.admin_view)
|
||||
|
||||
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
response = decorated_view(request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
def test_admin_required_decorator_with_redirect(self):
|
||||
# Apply the admin_required decorator to the view function
|
||||
decorated_view = admin_required(function=self.admin_view,redirect_to="/login/")
|
||||
|
||||
decorated_view = admin_required(function=self.admin_view, redirect_to="/login/")
|
||||
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
response = decorated_view(request)
|
||||
|
||||
# Assert redirection to login page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, '/login/')
|
||||
|
||||
self.assertEqual(response.url, "/login/")
|
||||
|
||||
@ -5,51 +5,51 @@ from accounts.decorators import admin_required, lecturer_required, student_requi
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class AdminRequiredDecoratorTests(TestCase):
|
||||
def setUp(self):
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username='admin', email='admin@example.com', password='password'
|
||||
username="admin", email="admin@example.com", password="password"
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username='user', email='user@example.com', password='password'
|
||||
username="user", email="user@example.com", password="password"
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
def admin_view(self, request):
|
||||
return HttpResponse("Admin View Content")
|
||||
|
||||
def test_admin_required_decorator_redirects(self):
|
||||
decorated_view = admin_required(self.admin_view)
|
||||
|
||||
|
||||
request = self.factory.get("/restricted-view")
|
||||
request.user = self.user
|
||||
response = decorated_view(request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/")
|
||||
|
||||
|
||||
def test_admin_required_decorator_redirects_to_correct_path(self):
|
||||
decorated_view = admin_required(function=self.admin_view,redirect_to="/login/")
|
||||
|
||||
decorated_view = admin_required(function=self.admin_view, redirect_to="/login/")
|
||||
|
||||
request = self.factory.get("restricted-view")
|
||||
request.user = self.user
|
||||
response = decorated_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, '/login/')
|
||||
|
||||
self.assertEqual(response.url, "/login/")
|
||||
|
||||
def test_admin_required_decorator_does_not_redirect_superuser(self):
|
||||
decorated_view = admin_required(self.admin_view)
|
||||
|
||||
|
||||
request = self.factory.get("/restricted-view")
|
||||
request.user = self.superuser
|
||||
response = decorated_view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content, b"Admin View Content")
|
||||
|
||||
|
||||
def test_admin_redirect_decorator_return_correct_response(self):
|
||||
decorated_view = admin_required(self.admin_view)
|
||||
|
||||
|
||||
request = self.factory.get("/restricted-view")
|
||||
request.user = self.superuser
|
||||
response = decorated_view(request)
|
||||
@ -59,10 +59,13 @@ class AdminRequiredDecoratorTests(TestCase):
|
||||
class LecturerRequiredDecoratorTests(TestCase):
|
||||
def setUp(self):
|
||||
self.lecturer = User.objects.create_user(
|
||||
username='lecturer', email='lecturer@example.com', password='password', is_lecturer=True
|
||||
username="lecturer",
|
||||
email="lecturer@example.com",
|
||||
password="password",
|
||||
is_lecturer=True,
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username='user', email='user@example.com', password='password'
|
||||
username="user", email="user@example.com", password="password"
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@ -81,7 +84,9 @@ class LecturerRequiredDecoratorTests(TestCase):
|
||||
self.assertEqual(response.url, "/")
|
||||
|
||||
def test_lecturer_required_decorator_redirects_to_correct_path(self):
|
||||
decorated_view = lecturer_required(function=self.lecturer_view, redirect_to="/login/")
|
||||
decorated_view = lecturer_required(
|
||||
function=self.lecturer_view, redirect_to="/login/"
|
||||
)
|
||||
|
||||
request = self.factory.get("/restricted-view")
|
||||
request.user = self.user
|
||||
@ -89,7 +94,7 @@ class LecturerRequiredDecoratorTests(TestCase):
|
||||
response = decorated_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, '/login/')
|
||||
self.assertEqual(response.url, "/login/")
|
||||
|
||||
def test_lecturer_required_decorator_does_not_redirect_lecturer(self):
|
||||
decorated_view = lecturer_required(self.lecturer_view)
|
||||
@ -112,13 +117,17 @@ class LecturerRequiredDecoratorTests(TestCase):
|
||||
|
||||
self.assertIsInstance(response, HttpResponse)
|
||||
|
||||
|
||||
class StudentRequiredDecoratorTests(TestCase):
|
||||
def setUp(self):
|
||||
self.student = User.objects.create_user(
|
||||
username='student', email='student@example.com', password='password', is_student=True
|
||||
username="student",
|
||||
email="student@example.com",
|
||||
password="password",
|
||||
is_student=True,
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username='user', email='user@example.com', password='password'
|
||||
username="user", email="user@example.com", password="password"
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@ -137,7 +146,9 @@ class StudentRequiredDecoratorTests(TestCase):
|
||||
self.assertEqual(response.url, "/")
|
||||
|
||||
def test_student_required_decorator_redirects_to_correct_path(self):
|
||||
decorated_view = student_required(function=self.student_view, redirect_to="/login/")
|
||||
decorated_view = student_required(
|
||||
function=self.student_view, redirect_to="/login/"
|
||||
)
|
||||
|
||||
request = self.factory.get("/restricted-view")
|
||||
request.user = self.user
|
||||
@ -145,7 +156,7 @@ class StudentRequiredDecoratorTests(TestCase):
|
||||
response = decorated_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, '/login/')
|
||||
self.assertEqual(response.url, "/login/")
|
||||
|
||||
def test_student_required_decorator_does_not_redirect_student(self):
|
||||
decorated_view = student_required(self.student_view)
|
||||
@ -166,4 +177,4 @@ class StudentRequiredDecoratorTests(TestCase):
|
||||
|
||||
response = decorated_view(request)
|
||||
|
||||
self.assertIsInstance(response, HttpResponse)
|
||||
self.assertIsInstance(response, HttpResponse)
|
||||
|
||||
@ -1,14 +1,30 @@
|
||||
from django.test import TestCase
|
||||
from accounts.filters import LecturerFilter, StudentFilter
|
||||
from accounts.filters import LecturerFilter, StudentFilter
|
||||
from accounts.models import User, Student
|
||||
from course.models import Program
|
||||
|
||||
|
||||
class LecturerFilterTestCase(TestCase):
|
||||
def setUp(self):
|
||||
User.objects.create(username="user1", first_name="John", last_name="Doe", email="john@example.com")
|
||||
User.objects.create(username="user2", first_name="Jane", last_name="Doe", email="jane@example.com")
|
||||
User.objects.create(username="user3", first_name="Alice", last_name="Smith", email="alice@example.com")
|
||||
|
||||
User.objects.create(
|
||||
username="user1",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
User.objects.create(
|
||||
username="user2",
|
||||
first_name="Jane",
|
||||
last_name="Doe",
|
||||
email="jane@example.com",
|
||||
)
|
||||
User.objects.create(
|
||||
username="user3",
|
||||
first_name="Alice",
|
||||
last_name="Smith",
|
||||
email="alice@example.com",
|
||||
)
|
||||
|
||||
def test_username_filter(self):
|
||||
filter_set = LecturerFilter(data={"username": "user1"})
|
||||
self.assertEqual(len(filter_set.qs), 1)
|
||||
@ -19,36 +35,80 @@ class LecturerFilterTestCase(TestCase):
|
||||
|
||||
def test_email_filter(self):
|
||||
filter_set = LecturerFilter(data={"email": "example.com"})
|
||||
self.assertEqual(len(filter_set.qs), 3) # All users should be returned since all have email addresses with "example.com"
|
||||
self.assertEqual(
|
||||
len(filter_set.qs), 3
|
||||
) # All users should be returned since all have email addresses with "example.com"
|
||||
|
||||
def test_combined_filters(self):
|
||||
filter_set = LecturerFilter(data={"name": "Doe", "email": "example.com"})
|
||||
self.assertEqual(len(filter_set.qs), 2) # Both John Doe and Jane Doe should be returned
|
||||
self.assertEqual(
|
||||
len(filter_set.qs), 2
|
||||
) # Both John Doe and Jane Doe should be returned
|
||||
|
||||
filter_set = LecturerFilter(data={"name": "Alice", "email": "example.com"})
|
||||
self.assertEqual(len(filter_set.qs), 1) # 1 user matches Alice with "example.com" in the email
|
||||
self.assertEqual(
|
||||
len(filter_set.qs), 1
|
||||
) # 1 user matches Alice with "example.com" in the email
|
||||
|
||||
def test_no_filters(self):
|
||||
filter_set = LecturerFilter(data={})
|
||||
self.assertEqual(len(filter_set.qs), 3) # All users should be returned since no filters are applied
|
||||
self.assertEqual(
|
||||
len(filter_set.qs), 3
|
||||
) # All users should be returned since no filters are applied
|
||||
|
||||
|
||||
class StudentFilterTestCase(TestCase):
|
||||
def setUp(self):
|
||||
program1 = Program.objects.create(title="Computer Science", summary="Program for computer science students")
|
||||
program2 = Program.objects.create(title="Mathematics", summary="Program for mathematics students")
|
||||
program3 = Program.objects.create(title="Computer Engineering", summary="Program for computer engineering students")
|
||||
program1 = Program.objects.create(
|
||||
title="Computer Science", summary="Program for computer science students"
|
||||
)
|
||||
program2 = Program.objects.create(
|
||||
title="Mathematics", summary="Program for mathematics students"
|
||||
)
|
||||
program3 = Program.objects.create(
|
||||
title="Computer Engineering",
|
||||
summary="Program for computer engineering students",
|
||||
)
|
||||
|
||||
Student.objects.create(student=User.objects.create(username="student1", first_name="John", last_name="Doe", email="john@example.com"), program=program1)
|
||||
Student.objects.create(student=User.objects.create(username="student2", first_name="Jane", last_name="Williams", email="jane@example.com"), program=program2)
|
||||
Student.objects.create(student=User.objects.create(username="student3", first_name="Alice", last_name="Smith", email="alice@example.com"), program=program3)
|
||||
Student.objects.create(
|
||||
student=User.objects.create(
|
||||
username="student1",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
program=program1,
|
||||
)
|
||||
Student.objects.create(
|
||||
student=User.objects.create(
|
||||
username="student2",
|
||||
first_name="Jane",
|
||||
last_name="Williams",
|
||||
email="jane@example.com",
|
||||
),
|
||||
program=program2,
|
||||
)
|
||||
Student.objects.create(
|
||||
student=User.objects.create(
|
||||
username="student3",
|
||||
first_name="Alice",
|
||||
last_name="Smith",
|
||||
email="alice@example.com",
|
||||
),
|
||||
program=program3,
|
||||
)
|
||||
|
||||
def test_name_filter(self):
|
||||
filtered_students = StudentFilter(data = {'name': 'John'}, queryset=Student.objects.all()).qs
|
||||
filtered_students = StudentFilter(
|
||||
data={"name": "John"}, queryset=Student.objects.all()
|
||||
).qs
|
||||
self.assertEqual(filtered_students.count(), 1)
|
||||
|
||||
|
||||
def test_email_filter(self):
|
||||
filter_set = StudentFilter(data={"email": "example.com"})
|
||||
self.assertEqual(len(filter_set.qs), 3) # All students should be returned since all have email addresses with "example.com"
|
||||
self.assertEqual(
|
||||
len(filter_set.qs), 3
|
||||
) # All students should be returned since all have email addresses with "example.com"
|
||||
|
||||
def test_program_filter(self):
|
||||
filter_set = StudentFilter(data={"program__title": "Computer Science"})
|
||||
|
||||
@ -37,7 +37,7 @@ urlpatterns = [
|
||||
path("", include("django.contrib.auth.urls")),
|
||||
path("admin_panel/", admin_panel, name="admin_panel"),
|
||||
path("profile/", profile, name="profile"),
|
||||
path("profile/<int:id>/detail/", profile_single, name="profile_single"),
|
||||
path("profile/<int:user_id>/detail/", profile_single, name="profile_single"),
|
||||
path("setting/", profile_update, name="edit_profile"),
|
||||
path("change_password/", change_password, name="change_password"),
|
||||
path("lecturers/", LecturerFilterView.as_view(), name="lecturer_list"),
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -1,34 +1,48 @@
|
||||
from django.http.response import JsonResponse
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.views.generic import CreateView, ListView
|
||||
from django.db.models import Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import get_template, render_to_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import CreateView
|
||||
from django_filters.views import FilterView
|
||||
from core.models import Session, Semester
|
||||
from course.models import Course
|
||||
from result.models import TakenCourse
|
||||
from .decorators import admin_required
|
||||
from .forms import (
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
from accounts.decorators import admin_required
|
||||
from accounts.filters import LecturerFilter, StudentFilter
|
||||
from accounts.forms import (
|
||||
ParentAddForm,
|
||||
ProfileUpdateForm,
|
||||
ProgramUpdateForm,
|
||||
StaffAddForm,
|
||||
StudentAddForm,
|
||||
ProfileUpdateForm,
|
||||
ParentAddForm,
|
||||
ProgramUpdateForm,
|
||||
)
|
||||
from .models import User, Student, Parent
|
||||
from .filters import LecturerFilter, StudentFilter
|
||||
from accounts.models import Parent, Student, User
|
||||
from core.models import Semester, Session
|
||||
from course.models import Course
|
||||
from result.models import TakenCourse
|
||||
|
||||
# to generate pdf from template we need the following
|
||||
from django.http import HttpResponse
|
||||
from django.template.loader import get_template # to get template which render as pdf
|
||||
from xhtml2pdf import pisa
|
||||
from django.template.loader import (
|
||||
render_to_string,
|
||||
) # to render a template into a string
|
||||
# ########################################################
|
||||
# Utility Functions
|
||||
# ########################################################
|
||||
|
||||
|
||||
def render_to_pdf(template_name, context):
|
||||
"""Render a given template to PDF format."""
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
response["Content-Disposition"] = 'filename="profile.pdf"'
|
||||
template = render_to_string(template_name, context)
|
||||
pdf = pisa.CreatePDF(template, dest=response)
|
||||
if pdf.err:
|
||||
return HttpResponse("We had some problems generating the PDF")
|
||||
return response
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Authentication and Registration
|
||||
# ########################################################
|
||||
|
||||
|
||||
def validate_username(request):
|
||||
@ -42,193 +56,125 @@ def register(request):
|
||||
form = StudentAddForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f"Account created successfuly.")
|
||||
else:
|
||||
messages.error(
|
||||
request, f"Somthing is not correct, please fill all fields correctly."
|
||||
)
|
||||
messages.success(request, "Account created successfully.")
|
||||
return redirect("login")
|
||||
messages.error(
|
||||
request, "Something is not correct, please fill all fields correctly."
|
||||
)
|
||||
else:
|
||||
form = StudentAddForm(request.POST)
|
||||
form = StudentAddForm()
|
||||
return render(request, "registration/register.html", {"form": form})
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Profile Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
"""Show profile of any user that fire out the request"""
|
||||
"""Show profile of the current user."""
|
||||
current_session = Session.objects.filter(is_current_session=True).first()
|
||||
current_semester = Semester.objects.filter(
|
||||
is_current_semester=True, session=current_session
|
||||
).first()
|
||||
|
||||
context = {
|
||||
"title": request.user.get_full_name,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
|
||||
if request.user.is_lecturer:
|
||||
courses = Course.objects.filter(
|
||||
allocated_course__lecturer__pk=request.user.id
|
||||
).filter(semester=current_semester)
|
||||
return render(
|
||||
request,
|
||||
"accounts/profile.html",
|
||||
{
|
||||
"title": request.user.get_full_name,
|
||||
"courses": courses,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
},
|
||||
allocated_course__lecturer__pk=request.user.id, semester=current_semester
|
||||
)
|
||||
elif request.user.is_student:
|
||||
level = Student.objects.get(student__pk=request.user.id)
|
||||
try:
|
||||
parent = Parent.objects.get(student=level)
|
||||
except:
|
||||
parent = "no parent set"
|
||||
courses = TakenCourse.objects.filter(
|
||||
student__student__id=request.user.id, course__level=level.level
|
||||
)
|
||||
context = {
|
||||
"title": request.user.get_full_name,
|
||||
"parent": parent,
|
||||
"courses": courses,
|
||||
"level": level,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
context["courses"] = courses
|
||||
return render(request, "accounts/profile.html", context)
|
||||
else:
|
||||
staff = User.objects.filter(is_lecturer=True)
|
||||
return render(
|
||||
request,
|
||||
"accounts/profile.html",
|
||||
{
|
||||
"title": request.user.get_full_name,
|
||||
"staff": staff,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
},
|
||||
|
||||
if request.user.is_student:
|
||||
student = get_object_or_404(Student, student__pk=request.user.id)
|
||||
parent = Parent.objects.filter(student=student).first()
|
||||
courses = TakenCourse.objects.filter(
|
||||
student__student__id=request.user.id, course__level=student.level
|
||||
)
|
||||
context.update(
|
||||
{
|
||||
"parent": parent,
|
||||
"courses": courses,
|
||||
"level": student.level,
|
||||
}
|
||||
)
|
||||
return render(request, "accounts/profile.html", context)
|
||||
|
||||
|
||||
# function that generate pdf by taking Django template and its context,
|
||||
def render_to_pdf(template_name, context):
|
||||
"""Renders a given template to PDF format."""
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
response["Content-Disposition"] = 'filename="profile.pdf"' # Set default filename
|
||||
|
||||
template = render_to_string(template_name, context)
|
||||
pdf = pisa.CreatePDF(template, dest=response)
|
||||
if pdf.err:
|
||||
return HttpResponse("We had some problems generating the PDF")
|
||||
|
||||
return response
|
||||
# For superuser or other staff
|
||||
staff = User.objects.filter(is_lecturer=True)
|
||||
context["staff"] = staff
|
||||
return render(request, "accounts/profile.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@admin_required
|
||||
def profile_single(request, id):
|
||||
"""Show profile of any selected user"""
|
||||
if request.user.id == id:
|
||||
return redirect("/profile/")
|
||||
def profile_single(request, user_id):
|
||||
"""Show profile of any selected user."""
|
||||
if request.user.id == user_id:
|
||||
return redirect("profile")
|
||||
|
||||
current_session = Session.objects.filter(is_current_session=True).first()
|
||||
current_semester = Semester.objects.filter(
|
||||
is_current_semester=True, session=current_session
|
||||
).first()
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
|
||||
user = User.objects.get(pk=id)
|
||||
"""
|
||||
If download_pdf exists, instead of calling render_to_pdf directly,
|
||||
pass the context dictionary built for the specific user type
|
||||
(lecturer, student, or superuser) to the render_to_pdf function.
|
||||
"""
|
||||
if request.GET.get("download_pdf"):
|
||||
if user.is_lecturer:
|
||||
courses = Course.objects.filter(allocated_course__lecturer__pk=id).filter(
|
||||
semester=current_semester
|
||||
)
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
|
||||
if user.is_lecturer:
|
||||
courses = Course.objects.filter(
|
||||
allocated_course__lecturer__pk=user_id, semester=current_semester
|
||||
)
|
||||
context.update(
|
||||
{
|
||||
"user_type": "Lecturer",
|
||||
"courses": courses,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
elif user.is_student:
|
||||
student = Student.objects.get(student__pk=id)
|
||||
courses = TakenCourse.objects.filter(
|
||||
student__student__id=id, course__level=student.level
|
||||
)
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
"user_type": "student",
|
||||
)
|
||||
elif user.is_student:
|
||||
student = get_object_or_404(Student, student__pk=user_id)
|
||||
courses = TakenCourse.objects.filter(
|
||||
student__student__id=user_id, course__level=student.level
|
||||
)
|
||||
context.update(
|
||||
{
|
||||
"user_type": "Student",
|
||||
"courses": courses,
|
||||
"student": student,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
else:
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
"user_type": "superuser",
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
)
|
||||
else:
|
||||
context["user_type"] = "Superuser"
|
||||
|
||||
if request.GET.get("download_pdf"):
|
||||
return render_to_pdf("pdf/profile_single.html", context)
|
||||
|
||||
else:
|
||||
if user.is_lecturer:
|
||||
courses = Course.objects.filter(allocated_course__lecturer__pk=id).filter(
|
||||
semester=current_semester
|
||||
)
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
"user_type": "Lecturer",
|
||||
"courses": courses,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
return render(request, "accounts/profile_single.html", context)
|
||||
elif user.is_student:
|
||||
student = Student.objects.get(student__pk=id)
|
||||
courses = TakenCourse.objects.filter(
|
||||
student__student__id=id, course__level=student.level
|
||||
)
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
"user_type": "student",
|
||||
"courses": courses,
|
||||
"student": student,
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
return render(request, "accounts/profile_single.html", context)
|
||||
else:
|
||||
context = {
|
||||
"title": user.get_full_name,
|
||||
"user": user,
|
||||
"user_type": "superuser",
|
||||
"current_session": current_session,
|
||||
"current_semester": current_semester,
|
||||
}
|
||||
return render(request, "accounts/profile_single.html", context)
|
||||
return render(request, "accounts/profile_single.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_panel(request):
|
||||
return render(
|
||||
request, "setting/admin_panel.html", {"title": request.user.get_full_name}
|
||||
)
|
||||
return render(request, "setting/admin_panel.html", {"title": "Admin Panel"})
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Settings Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Setting views
|
||||
# ########################################################
|
||||
@login_required
|
||||
def profile_update(request):
|
||||
if request.method == "POST":
|
||||
@ -237,18 +183,10 @@ def profile_update(request):
|
||||
form.save()
|
||||
messages.success(request, "Your profile has been updated successfully.")
|
||||
return redirect("profile")
|
||||
else:
|
||||
messages.error(request, "Please correct the error(s) below.")
|
||||
messages.error(request, "Please correct the error(s) below.")
|
||||
else:
|
||||
form = ProfileUpdateForm(instance=request.user)
|
||||
return render(
|
||||
request,
|
||||
"setting/profile_info_change.html",
|
||||
{
|
||||
"title": "Setting",
|
||||
"form": form,
|
||||
},
|
||||
)
|
||||
return render(request, "setting/profile_info_change.html", {"form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
@ -260,20 +198,15 @@ def change_password(request):
|
||||
update_session_auth_hash(request, user)
|
||||
messages.success(request, "Your password was successfully updated!")
|
||||
return redirect("profile")
|
||||
else:
|
||||
messages.error(request, "Please correct the error(s) below. ")
|
||||
messages.error(request, "Please correct the error(s) below.")
|
||||
else:
|
||||
form = PasswordChangeForm(request.user)
|
||||
return render(
|
||||
request,
|
||||
"setting/password_change.html",
|
||||
{
|
||||
"form": form,
|
||||
},
|
||||
)
|
||||
return render(request, "setting/password_change.html", {"form": form})
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Staff (Lecturer) Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@login_required
|
||||
@ -281,58 +214,39 @@ def change_password(request):
|
||||
def staff_add_view(request):
|
||||
if request.method == "POST":
|
||||
form = StaffAddForm(request.POST)
|
||||
first_name = request.POST.get("first_name")
|
||||
last_name = request.POST.get("last_name")
|
||||
email = request.POST.get("email")
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
form.save()
|
||||
lecturer = form.save()
|
||||
full_name = lecturer.get_full_name
|
||||
email = lecturer.email
|
||||
messages.success(
|
||||
request,
|
||||
"Account for lecturer "
|
||||
+ first_name
|
||||
+ " "
|
||||
+ last_name
|
||||
+ " has been created. An email with account credentials will be sent to "
|
||||
+ email
|
||||
+ " within a minute.",
|
||||
f"Account for lecturer {full_name} has been created. "
|
||||
f"An email with account credentials will be sent to {email} within a minute.",
|
||||
)
|
||||
return redirect("lecturer_list")
|
||||
else:
|
||||
form = StaffAddForm()
|
||||
|
||||
context = {
|
||||
"title": "Lecturer Add",
|
||||
"form": form,
|
||||
}
|
||||
|
||||
return render(request, "accounts/add_staff.html", context)
|
||||
return render(
|
||||
request, "accounts/add_staff.html", {"title": "Add Lecturer", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_staff(request, pk):
|
||||
instance = get_object_or_404(User, is_lecturer=True, pk=pk)
|
||||
lecturer = get_object_or_404(User, is_lecturer=True, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = ProfileUpdateForm(request.POST, request.FILES, instance=instance)
|
||||
full_name = instance.get_full_name
|
||||
form = ProfileUpdateForm(request.POST, request.FILES, instance=lecturer)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
messages.success(request, "Lecturer " + full_name + " has been updated.")
|
||||
full_name = lecturer.get_full_name
|
||||
messages.success(request, f"Lecturer {full_name} has been updated.")
|
||||
return redirect("lecturer_list")
|
||||
else:
|
||||
messages.error(request, "Please correct the error below.")
|
||||
messages.error(request, "Please correct the error below.")
|
||||
else:
|
||||
form = ProfileUpdateForm(instance=instance)
|
||||
form = ProfileUpdateForm(instance=lecturer)
|
||||
return render(
|
||||
request,
|
||||
"accounts/edit_lecturer.html",
|
||||
{
|
||||
"title": "Edit Lecturer",
|
||||
"form": form,
|
||||
},
|
||||
request, "accounts/edit_lecturer.html", {"title": "Edit Lecturer", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@ -341,7 +255,7 @@ class LecturerFilterView(FilterView):
|
||||
filterset_class = LecturerFilter
|
||||
queryset = User.objects.filter(is_lecturer=True)
|
||||
template_name = "accounts/lecturer_list.html"
|
||||
paginate_by = 10 # if pagination is desired
|
||||
paginate_by = 10
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -349,107 +263,76 @@ class LecturerFilterView(FilterView):
|
||||
return context
|
||||
|
||||
|
||||
# lecturers list pdf
|
||||
@login_required
|
||||
@admin_required
|
||||
def render_lecturer_pdf_list(request):
|
||||
lecturers = User.objects.filter(is_lecturer=True)
|
||||
template_path = "pdf/lecturer_list.html"
|
||||
context = {"lecturers": lecturers}
|
||||
response = HttpResponse(
|
||||
content_type="application/pdf"
|
||||
) # convert the response to pdf
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
response["Content-Disposition"] = 'filename="lecturers_list.pdf"'
|
||||
# find the template and render it.
|
||||
template = get_template(template_path)
|
||||
html = template.render(context)
|
||||
# create a pdf
|
||||
pisa_status = pisa.CreatePDF(html, dest=response)
|
||||
# if error then show some funny view
|
||||
if pisa_status.err:
|
||||
return HttpResponse("We had some errors <pre>" + html + "</pre>")
|
||||
return HttpResponse(f"We had some errors <pre>{html}</pre>")
|
||||
return response
|
||||
|
||||
|
||||
# @login_required
|
||||
# @lecturer_required
|
||||
# def delete_staff(request, pk):
|
||||
# staff = get_object_or_404(User, pk=pk)
|
||||
# staff.delete()
|
||||
# return redirect('lecturer_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_staff(request, pk):
|
||||
lecturer = get_object_or_404(User, pk=pk)
|
||||
lecturer = get_object_or_404(User, is_lecturer=True, pk=pk)
|
||||
full_name = lecturer.get_full_name
|
||||
lecturer.delete()
|
||||
messages.success(request, "Lecturer " + full_name + " has been deleted.")
|
||||
messages.success(request, f"Lecturer {full_name} has been deleted.")
|
||||
return redirect("lecturer_list")
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Student Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Student views
|
||||
# ########################################################
|
||||
@login_required
|
||||
@admin_required
|
||||
def student_add_view(request):
|
||||
if request.method == "POST":
|
||||
form = StudentAddForm(request.POST)
|
||||
first_name = request.POST.get("first_name")
|
||||
last_name = request.POST.get("last_name")
|
||||
email = request.POST.get("email")
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
student = form.save()
|
||||
full_name = student.get_full_name
|
||||
email = student.email
|
||||
messages.success(
|
||||
request,
|
||||
"Account for "
|
||||
+ first_name
|
||||
+ " "
|
||||
+ last_name
|
||||
+ " has been created. An email with account credentials will be sent to "
|
||||
+ email
|
||||
+ " within a minute.",
|
||||
f"Account for {full_name} has been created. "
|
||||
f"An email with account credentials will be sent to {email} within a minute.",
|
||||
)
|
||||
return redirect("student_list")
|
||||
else:
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = StudentAddForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/add_student.html",
|
||||
{"title": "Add Student", "form": form},
|
||||
request, "accounts/add_student.html", {"title": "Add Student", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_student(request, pk):
|
||||
# instance = User.objects.get(pk=pk)
|
||||
instance = get_object_or_404(User, is_student=True, pk=pk)
|
||||
student_user = get_object_or_404(User, is_student=True, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = ProfileUpdateForm(request.POST, request.FILES, instance=instance)
|
||||
full_name = instance.get_full_name
|
||||
form = ProfileUpdateForm(request.POST, request.FILES, instance=student_user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
messages.success(request, ("Student " + full_name + " has been updated."))
|
||||
full_name = student_user.get_full_name
|
||||
messages.success(request, f"Student {full_name} has been updated.")
|
||||
return redirect("student_list")
|
||||
else:
|
||||
messages.error(request, "Please correct the error below.")
|
||||
messages.error(request, "Please correct the error below.")
|
||||
else:
|
||||
form = ProfileUpdateForm(instance=instance)
|
||||
form = ProfileUpdateForm(instance=student_user)
|
||||
return render(
|
||||
request,
|
||||
"accounts/edit_student.html",
|
||||
{
|
||||
"title": "Edit-profile",
|
||||
"form": form,
|
||||
},
|
||||
request, "accounts/edit_student.html", {"title": "Edit Student", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@ -466,23 +349,19 @@ class StudentListView(FilterView):
|
||||
return context
|
||||
|
||||
|
||||
# student list pdf
|
||||
@login_required
|
||||
@admin_required
|
||||
def render_student_pdf_list(request):
|
||||
students = Student.objects.all()
|
||||
template_path = "pdf/student_list.html"
|
||||
context = {"students": students}
|
||||
response = HttpResponse(
|
||||
content_type="application/pdf"
|
||||
) # convert the response to pdf
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
response["Content-Disposition"] = 'filename="students_list.pdf"'
|
||||
# find the template and render it.
|
||||
template = get_template(template_path)
|
||||
html = template.render(context)
|
||||
# create a pdf
|
||||
pisa_status = pisa.CreatePDF(html, dest=response)
|
||||
# if error then show some funny view
|
||||
if pisa_status.err:
|
||||
return HttpResponse("We had some errors <pre>" + html + "</pre>")
|
||||
return HttpResponse(f"We had some errors <pre>{html}</pre>")
|
||||
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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
|
||||
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__)))
|
||||
@ -104,9 +105,6 @@ TEMPLATES = [
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
ASGI_APPLICATION = "config.asgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
@ -142,7 +140,10 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
gettext = lambda s: s
|
||||
|
||||
def gettext(s):
|
||||
return s
|
||||
|
||||
|
||||
LANGUAGES = (
|
||||
("en", gettext("English")),
|
||||
@ -251,3 +252,32 @@ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
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")),
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
|
||||
from .models import NewsAndEvents, Session, Semester, SEMESTER
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
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
|
||||
@ -30,3 +33,25 @@ 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 = f"{slug}-{random_string_generator(size=4)}"
|
||||
return unique_slug_generator(instance, new_slug=new_slug)
|
||||
return slug
|
||||
|
||||
209
core/views.py
209
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()
|
||||
|
||||
@ -51,7 +51,6 @@ class CourseAllocationForm(forms.ModelForm):
|
||||
fields = ["lecturer", "courses"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user")
|
||||
super(CourseAllocationForm, self).__init__(*args, **kwargs)
|
||||
self.fields["lecturer"].queryset = User.objects.filter(is_lecturer=True)
|
||||
|
||||
|
||||
@ -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,
|
||||
),
|
||||
]
|
||||
174
course/models.py
174
course/models.py
@ -1,145 +1,104 @@
|
||||
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
|
||||
|
||||
YEARS = (
|
||||
(1, "1"),
|
||||
(2, "2"),
|
||||
(3, "3"),
|
||||
(4, "4"),
|
||||
(4, "5"),
|
||||
(4, "6"),
|
||||
)
|
||||
|
||||
# LEVEL_COURSE = "Level course"
|
||||
BACHELOR_DEGREE = _("Bachelor")
|
||||
MASTER_DEGREE = _("Master")
|
||||
|
||||
LEVEL = (
|
||||
# (LEVEL_COURSE, "Level course"),
|
||||
(BACHELOR_DEGREE, _("Bachelor Degree")),
|
||||
(MASTER_DEGREE, _("Master Degree")),
|
||||
)
|
||||
|
||||
FIRST = _("First")
|
||||
SECOND = _("Second")
|
||||
THIRD = _("Third")
|
||||
|
||||
SEMESTER = (
|
||||
(FIRST, _("First")),
|
||||
(SECOND, _("Second")),
|
||||
(THIRD, _("Third")),
|
||||
)
|
||||
from core.models import ActivityLog, Semester
|
||||
from core.utils import unique_slug_generator
|
||||
|
||||
|
||||
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=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()
|
||||
|
||||
def __str__(self):
|
||||
return "{0} ({1})".format(self.title, self.code)
|
||||
return f"{self.title} ({self.code})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("course_detail", kwargs={"slug": self.slug})
|
||||
|
||||
@property
|
||||
def is_current_semester(self):
|
||||
from core.models import Semester
|
||||
|
||||
current_semester = Semester.objects.get(is_current_semester=True)
|
||||
|
||||
if self.semester == current_semester.semester:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
current_semester = Semester.objects.filter(is_current_semester=True).first()
|
||||
return self.semester == current_semester.semester if current_semester else False
|
||||
|
||||
|
||||
def course_pre_save_receiver(sender, instance, *args, **kwargs):
|
||||
@receiver(pre_save, sender=Course)
|
||||
def course_pre_save_receiver(sender, instance, **kwargs):
|
||||
if not instance.slug:
|
||||
instance.slug = unique_slug_generator(instance)
|
||||
|
||||
|
||||
pre_save.connect(course_pre_save_receiver, sender=Course)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Course)
|
||||
def log_save(sender, instance, created, **kwargs):
|
||||
def log_course_save(sender, instance, created, **kwargs):
|
||||
verb = "created" if created else "updated"
|
||||
ActivityLog.objects.create(message=_(f"The course '{instance}' has been {verb}."))
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Course)
|
||||
def log_delete(sender, instance, **kwargs):
|
||||
def log_course_delete(sender, instance, **kwargs):
|
||||
ActivityLog.objects.create(message=_(f"The course '{instance}' has been deleted."))
|
||||
|
||||
|
||||
@ -147,9 +106,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 +125,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 +145,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 +163,28 @@ class Upload(models.Model):
|
||||
return "powerpoint"
|
||||
elif ext in ("zip", "rar", "7zip"):
|
||||
return "archive"
|
||||
return "file"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.file.delete()
|
||||
self.file.delete(save=False)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Upload)
|
||||
def log_save(sender, instance, created, **kwargs):
|
||||
def log_upload_save(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
ActivityLog.objects.create(
|
||||
message=_(
|
||||
f"The file '{instance.title}' has been uploaded to the course '{instance.course}'."
|
||||
)
|
||||
message = _(
|
||||
f"The file '{instance.title}' has been uploaded to the course '{instance.course}'."
|
||||
)
|
||||
else:
|
||||
ActivityLog.objects.create(
|
||||
message=_(
|
||||
f"The file '{instance.title}' of the course '{instance.course}' has been updated."
|
||||
)
|
||||
message = _(
|
||||
f"The file '{instance.title}' of the course '{instance.course}' has been updated."
|
||||
)
|
||||
ActivityLog.objects.create(message=message)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Upload)
|
||||
def log_delete(sender, instance, **kwargs):
|
||||
def log_upload_delete(sender, instance, **kwargs):
|
||||
ActivityLog.objects.create(
|
||||
message=_(
|
||||
f"The file '{instance.title}' of the course '{instance.course}' has been deleted."
|
||||
@ -237,7 +194,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 +203,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 +215,31 @@ class UploadVideo(models.Model):
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.video.delete()
|
||||
self.video.delete(save=False)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
def video_pre_save_receiver(sender, instance, *args, **kwargs):
|
||||
@receiver(pre_save, sender=UploadVideo)
|
||||
def video_pre_save_receiver(sender, instance, **kwargs):
|
||||
if not instance.slug:
|
||||
instance.slug = unique_slug_generator(instance)
|
||||
|
||||
|
||||
pre_save.connect(video_pre_save_receiver, sender=UploadVideo)
|
||||
|
||||
|
||||
@receiver(post_save, sender=UploadVideo)
|
||||
def log_save(sender, instance, created, **kwargs):
|
||||
def log_uploadvideo_save(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
ActivityLog.objects.create(
|
||||
message=_(
|
||||
f"The video '{instance.title}' has been uploaded to the course {instance.course}."
|
||||
)
|
||||
message = _(
|
||||
f"The video '{instance.title}' has been uploaded to the course '{instance.course}'."
|
||||
)
|
||||
else:
|
||||
ActivityLog.objects.create(
|
||||
message=_(
|
||||
f"The video '{instance.title}' of the course '{instance.course}' has been updated."
|
||||
)
|
||||
message = _(
|
||||
f"The video '{instance.title}' of the course '{instance.course}' has been updated."
|
||||
)
|
||||
ActivityLog.objects.create(message=message)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=UploadVideo)
|
||||
def log_delete(sender, instance, **kwargs):
|
||||
def log_uploadvideo_delete(sender, instance, **kwargs):
|
||||
ActivityLog.objects.create(
|
||||
message=_(
|
||||
f"The video '{instance.title}' of the course '{instance.course}' has been deleted."
|
||||
@ -296,9 +248,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)
|
||||
|
||||
@ -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("<int:pk>/detail/", program_detail, name="program_detail"),
|
||||
path("add/", program_add, name="add_program"),
|
||||
path("<int:pk>/edit/", program_edit, name="edit_program"),
|
||||
path("<int:pk>/delete/", program_delete, name="program_delete"),
|
||||
path("", views.ProgramFilterView.as_view(), name="programs"),
|
||||
path("<int:pk>/detail/", views.program_detail, name="program_detail"),
|
||||
path("add/", views.program_add, name="add_program"),
|
||||
path("<int:pk>/edit/", views.program_edit, name="edit_program"),
|
||||
path("<int:pk>/delete/", views.program_delete, name="program_delete"),
|
||||
# Course urls
|
||||
path("course/<slug>/detail/", course_single, name="course_detail"),
|
||||
path("<int:pk>/course/add/", course_add, name="course_add"),
|
||||
path("course/<slug>/edit/", course_edit, name="edit_course"),
|
||||
path("course/delete/<slug>/", course_delete, name="delete_course"),
|
||||
path("course/<slug>/detail/", views.course_single, name="course_detail"),
|
||||
path("<int:pk>/course/add/", views.course_add, name="course_add"),
|
||||
path("course/<slug>/edit/", views.course_edit, name="edit_course"),
|
||||
path("course/delete/<slug>/", 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/<int:pk>/edit/",
|
||||
edit_allocated_course,
|
||||
views.edit_allocated_course,
|
||||
name="edit_allocated_course",
|
||||
),
|
||||
path("course/<int:pk>/deallocate/", deallocate_course, name="course_deallocate"),
|
||||
path(
|
||||
"course/<int:pk>/deallocate/", views.deallocate_course, name="course_deallocate"
|
||||
),
|
||||
# File uploads urls
|
||||
path(
|
||||
"course/<slug>/documentations/upload/",
|
||||
handle_file_upload,
|
||||
views.handle_file_upload,
|
||||
name="upload_file_view",
|
||||
),
|
||||
path(
|
||||
"course/<slug>/documentations/<int:file_id>/edit/",
|
||||
handle_file_edit,
|
||||
views.handle_file_edit,
|
||||
name="upload_file_edit",
|
||||
),
|
||||
path(
|
||||
"course/<slug>/documentations/<int:file_id>/delete/",
|
||||
handle_file_delete,
|
||||
views.handle_file_delete,
|
||||
name="upload_file_delete",
|
||||
),
|
||||
# Video uploads urls
|
||||
path(
|
||||
"course/<slug>/video_tutorials/upload/",
|
||||
handle_video_upload,
|
||||
views.handle_video_upload,
|
||||
name="upload_video",
|
||||
),
|
||||
path(
|
||||
"course/<slug>/video_tutorials/<video_slug>/detail/",
|
||||
handle_video_single,
|
||||
views.handle_video_single,
|
||||
name="video_single",
|
||||
),
|
||||
path(
|
||||
"course/<slug>/video_tutorials/<video_slug>/edit/",
|
||||
handle_video_edit,
|
||||
views.handle_video_edit,
|
||||
name="upload_video_edit",
|
||||
),
|
||||
path(
|
||||
"course/<slug>/video_tutorials/<video_slug>/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"),
|
||||
]
|
||||
|
||||
@ -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
|
||||
435
course/views.py
435
course/views.py
@ -1,28 +1,38 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.db.models import Sum, Avg, Max, Min, Count
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.generic import CreateView
|
||||
from django.core.paginator import Paginator
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Sum
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import CreateView
|
||||
from django_filters.views import FilterView
|
||||
|
||||
from accounts.models import User, Student
|
||||
from core.models import Session, Semester
|
||||
from result.models import TakenCourse
|
||||
from accounts.decorators import lecturer_required, student_required
|
||||
from .forms import (
|
||||
ProgramForm,
|
||||
from accounts.models import Student
|
||||
from core.models import Semester
|
||||
from course.filters import CourseAllocationFilter, ProgramFilter
|
||||
from course.forms import (
|
||||
CourseAddForm,
|
||||
CourseAllocationForm,
|
||||
EditCourseAllocationForm,
|
||||
ProgramForm,
|
||||
UploadFormFile,
|
||||
UploadFormVideo,
|
||||
)
|
||||
from .filters import ProgramFilter, CourseAllocationFilter
|
||||
from .models import Program, Course, CourseAllocation, Upload, UploadVideo
|
||||
from course.models import (
|
||||
Course,
|
||||
CourseAllocation,
|
||||
Program,
|
||||
Upload,
|
||||
UploadVideo,
|
||||
)
|
||||
from result.models import TakenCourse
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Program Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
@ -42,37 +52,25 @@ def program_add(request):
|
||||
if request.method == "POST":
|
||||
form = ProgramForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request, request.POST.get("title") + " program has been created."
|
||||
)
|
||||
program = form.save()
|
||||
messages.success(request, f"{program.title} program has been created.")
|
||||
return redirect("programs")
|
||||
else:
|
||||
messages.error(request, "Correct the error(S) below.")
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = ProgramForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"course/program_add.html",
|
||||
{
|
||||
"title": "Add Program",
|
||||
"form": form,
|
||||
},
|
||||
request, "course/program_add.html", {"title": "Add Program", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def program_detail(request, pk):
|
||||
program = Program.objects.get(pk=pk)
|
||||
program = get_object_or_404(Program, pk=pk)
|
||||
courses = Course.objects.filter(program_id=pk).order_by("-year")
|
||||
credits = Course.objects.aggregate(Sum("credit"))
|
||||
|
||||
credits = courses.aggregate(total_credits=Sum("credit"))
|
||||
paginator = Paginator(courses, 10)
|
||||
page = request.GET.get("page")
|
||||
|
||||
courses = paginator.get_page(page)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"course/program_single.html",
|
||||
@ -88,52 +86,42 @@ def program_detail(request, pk):
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def program_edit(request, pk):
|
||||
program = Program.objects.get(pk=pk)
|
||||
|
||||
program = get_object_or_404(Program, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = ProgramForm(request.POST, instance=program)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request, str(request.POST.get("title")) + " program has been updated."
|
||||
)
|
||||
program = form.save()
|
||||
messages.success(request, f"{program.title} program has been updated.")
|
||||
return redirect("programs")
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = ProgramForm(instance=program)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"course/program_add.html",
|
||||
{"title": "Edit Program", "form": form},
|
||||
request, "course/program_add.html", {"title": "Edit Program", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def program_delete(request, pk):
|
||||
program = Program.objects.get(pk=pk)
|
||||
program = get_object_or_404(Program, pk=pk)
|
||||
title = program.title
|
||||
program.delete()
|
||||
messages.success(request, "Program " + title + " has been deleted.")
|
||||
|
||||
messages.success(request, f"Program {title} has been deleted.")
|
||||
return redirect("programs")
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Course Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Course views
|
||||
# ########################################################
|
||||
@login_required
|
||||
def course_single(request, slug):
|
||||
course = Course.objects.get(slug=slug)
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
files = Upload.objects.filter(course__slug=slug)
|
||||
videos = UploadVideo.objects.filter(course__slug=slug)
|
||||
|
||||
# lecturers = User.objects.filter(allocated_lecturer__pk=course.id)
|
||||
lecturers = CourseAllocation.objects.filter(courses__pk=course.id)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"course/course_single.html",
|
||||
@ -143,7 +131,7 @@ def course_single(request, slug):
|
||||
"files": files,
|
||||
"videos": videos,
|
||||
"lecturers": lecturers,
|
||||
"media_url": settings.MEDIA_ROOT,
|
||||
"media_url": settings.MEDIA_URL,
|
||||
},
|
||||
)
|
||||
|
||||
@ -151,31 +139,22 @@ def course_single(request, slug):
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def course_add(request, pk):
|
||||
users = User.objects.all()
|
||||
program = get_object_or_404(Program, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = CourseAddForm(request.POST)
|
||||
course_name = request.POST.get("title")
|
||||
course_code = request.POST.get("code")
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
course = form.save()
|
||||
messages.success(
|
||||
request, (course_name + "(" + course_code + ")" + " has been created.")
|
||||
request, f"{course.title} ({course.code}) has been created."
|
||||
)
|
||||
return redirect("program_detail", pk=request.POST.get("program"))
|
||||
else:
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
return redirect("program_detail", pk=program.pk)
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = CourseAddForm(initial={"program": Program.objects.get(pk=pk)})
|
||||
|
||||
form = CourseAddForm(initial={"program": program})
|
||||
return render(
|
||||
request,
|
||||
"course/course_add.html",
|
||||
{
|
||||
"title": "Add Course",
|
||||
"form": form,
|
||||
"program": pk,
|
||||
"users": users,
|
||||
},
|
||||
{"title": "Add Course", "form": form, "program": program},
|
||||
)
|
||||
|
||||
|
||||
@ -185,73 +164,49 @@ def course_edit(request, slug):
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
if request.method == "POST":
|
||||
form = CourseAddForm(request.POST, instance=course)
|
||||
course_name = request.POST.get("title")
|
||||
course_code = request.POST.get("code")
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
course = form.save()
|
||||
messages.success(
|
||||
request, (course_name + "(" + course_code + ")" + " has been updated.")
|
||||
request, f"{course.title} ({course.code}) has been updated."
|
||||
)
|
||||
return redirect("program_detail", pk=request.POST.get("program"))
|
||||
else:
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
return redirect("program_detail", pk=course.program.pk)
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = CourseAddForm(instance=course)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"course/course_add.html",
|
||||
{
|
||||
"title": "Edit Course",
|
||||
# 'form': form, 'program': pk, 'course': pk
|
||||
"form": form,
|
||||
},
|
||||
request, "course/course_add.html", {"title": "Edit Course", "form": form}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def course_delete(request, slug):
|
||||
course = Course.objects.get(slug=slug)
|
||||
# course_name = course.title
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
title = course.title
|
||||
program_id = course.program.id
|
||||
course.delete()
|
||||
messages.success(request, "Course " + course.title + " has been deleted.")
|
||||
|
||||
return redirect("program_detail", pk=course.program.id)
|
||||
messages.success(request, f"Course {title} has been deleted.")
|
||||
return redirect("program_detail", pk=program_id)
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Course Allocation Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Course Allocation
|
||||
# ########################################################
|
||||
@method_decorator([login_required], name="dispatch")
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class CourseAllocationFormView(CreateView):
|
||||
form_class = CourseAllocationForm
|
||||
template_name = "course/course_allocation_form.html"
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(CourseAllocationFormView, self).get_form_kwargs()
|
||||
kwargs["user"] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
# if a staff has been allocated a course before update it else create new
|
||||
lecturer = form.cleaned_data["lecturer"]
|
||||
selected_courses = form.cleaned_data["courses"]
|
||||
courses = ()
|
||||
for course in selected_courses:
|
||||
courses += (course.pk,)
|
||||
# print(courses)
|
||||
|
||||
try:
|
||||
a = CourseAllocation.objects.get(lecturer=lecturer)
|
||||
except:
|
||||
a = CourseAllocation.objects.create(lecturer=lecturer)
|
||||
for i in range(0, selected_courses.count()):
|
||||
a.courses.add(courses[i])
|
||||
a.save()
|
||||
allocation, created = CourseAllocation.objects.get_or_create(lecturer=lecturer)
|
||||
allocation.courses.set(selected_courses)
|
||||
messages.success(
|
||||
self.request, f"Courses allocated to {lecturer.get_full_name} successfully."
|
||||
)
|
||||
return redirect("course_allocation_view")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -260,7 +215,7 @@ class CourseAllocationFormView(CreateView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator([login_required], name="dispatch")
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class CourseAllocationFilterView(FilterView):
|
||||
filterset_class = CourseAllocationFilter
|
||||
template_name = "course/course_allocation_view.html"
|
||||
@ -274,53 +229,50 @@ class CourseAllocationFilterView(FilterView):
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def edit_allocated_course(request, pk):
|
||||
allocated = get_object_or_404(CourseAllocation, pk=pk)
|
||||
allocation = get_object_or_404(CourseAllocation, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = EditCourseAllocationForm(request.POST, instance=allocated)
|
||||
form = EditCourseAllocationForm(request.POST, instance=allocation)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "course assigned has been updated.")
|
||||
messages.success(request, "Course allocation has been updated.")
|
||||
return redirect("course_allocation_view")
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = EditCourseAllocationForm(instance=allocated)
|
||||
|
||||
form = EditCourseAllocationForm(instance=allocation)
|
||||
return render(
|
||||
request,
|
||||
"course/course_allocation_form.html",
|
||||
{"title": "Edit Course Allocated", "form": form, "allocated": pk},
|
||||
{"title": "Edit Course Allocation", "form": form},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def deallocate_course(request, pk):
|
||||
course = CourseAllocation.objects.get(pk=pk)
|
||||
course.delete()
|
||||
messages.success(request, "successfully deallocate!")
|
||||
allocation = get_object_or_404(CourseAllocation, pk=pk)
|
||||
allocation.delete()
|
||||
messages.success(request, "Successfully deallocated courses.")
|
||||
return redirect("course_allocation_view")
|
||||
|
||||
|
||||
# ########################################################
|
||||
# File Upload Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
# ########################################################
|
||||
# File Upload views
|
||||
# ########################################################
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def handle_file_upload(request, slug):
|
||||
course = Course.objects.get(slug=slug)
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
if request.method == "POST":
|
||||
form = UploadFormFile(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj.course = course
|
||||
obj.save()
|
||||
|
||||
messages.success(
|
||||
request, (request.POST.get("title") + " has been uploaded.")
|
||||
)
|
||||
upload = form.save(commit=False)
|
||||
upload.course = course
|
||||
upload.save()
|
||||
messages.success(request, f"{upload.title} has been uploaded.")
|
||||
return redirect("course_detail", slug=slug)
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = UploadFormFile()
|
||||
return render(
|
||||
@ -333,54 +285,52 @@ def handle_file_upload(request, slug):
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def handle_file_edit(request, slug, file_id):
|
||||
course = Course.objects.get(slug=slug)
|
||||
instance = Upload.objects.get(pk=file_id)
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
upload = get_object_or_404(Upload, pk=file_id)
|
||||
if request.method == "POST":
|
||||
form = UploadFormFile(request.POST, request.FILES, instance=instance)
|
||||
# file_name = request.POST.get('name')
|
||||
form = UploadFormFile(request.POST, request.FILES, instance=upload)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request, (request.POST.get("title") + " has been updated.")
|
||||
)
|
||||
upload = form.save()
|
||||
messages.success(request, f"{upload.title} has been updated.")
|
||||
return redirect("course_detail", slug=slug)
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = UploadFormFile(instance=instance)
|
||||
|
||||
form = UploadFormFile(instance=upload)
|
||||
return render(
|
||||
request,
|
||||
"upload/upload_file_form.html",
|
||||
{"title": instance.title, "form": form, "course": course},
|
||||
{"title": "Edit File", "form": form, "course": course},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def handle_file_delete(request, slug, file_id):
|
||||
file = Upload.objects.get(pk=file_id)
|
||||
# file_name = file.name
|
||||
file.delete()
|
||||
|
||||
messages.success(request, (file.title + " has been deleted."))
|
||||
upload = get_object_or_404(Upload, pk=file_id)
|
||||
title = upload.title
|
||||
upload.delete()
|
||||
messages.success(request, f"{title} has been deleted.")
|
||||
return redirect("course_detail", slug=slug)
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Video Upload views
|
||||
# Video Upload Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def handle_video_upload(request, slug):
|
||||
course = Course.objects.get(slug=slug)
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
if request.method == "POST":
|
||||
form = UploadFormVideo(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj.course = course
|
||||
obj.save()
|
||||
|
||||
messages.success(
|
||||
request, (request.POST.get("title") + " has been uploaded.")
|
||||
)
|
||||
video = form.save(commit=False)
|
||||
video.course = course
|
||||
video.save()
|
||||
messages.success(request, f"{video.title} has been uploaded.")
|
||||
return redirect("course_detail", slug=slug)
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = UploadFormVideo()
|
||||
return render(
|
||||
@ -391,172 +341,123 @@ def handle_video_upload(request, slug):
|
||||
|
||||
|
||||
@login_required
|
||||
# @lecturer_required
|
||||
def handle_video_single(request, slug, video_slug):
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
video = get_object_or_404(UploadVideo, slug=video_slug)
|
||||
return render(request, "upload/video_single.html", {"video": video})
|
||||
return render(
|
||||
request,
|
||||
"upload/video_single.html",
|
||||
{"video": video, "course": course},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def handle_video_edit(request, slug, video_slug):
|
||||
course = Course.objects.get(slug=slug)
|
||||
instance = UploadVideo.objects.get(slug=video_slug)
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
video = get_object_or_404(UploadVideo, slug=video_slug)
|
||||
if request.method == "POST":
|
||||
form = UploadFormVideo(request.POST, request.FILES, instance=instance)
|
||||
form = UploadFormVideo(request.POST, request.FILES, instance=video)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request, (request.POST.get("title") + " has been updated.")
|
||||
)
|
||||
video = form.save()
|
||||
messages.success(request, f"{video.title} has been updated.")
|
||||
return redirect("course_detail", slug=slug)
|
||||
messages.error(request, "Correct the error(s) below.")
|
||||
else:
|
||||
form = UploadFormVideo(instance=instance)
|
||||
|
||||
form = UploadFormVideo(instance=video)
|
||||
return render(
|
||||
request,
|
||||
"upload/upload_video_form.html",
|
||||
{"title": instance.title, "form": form, "course": course},
|
||||
{"title": "Edit Video", "form": form, "course": course},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def handle_video_delete(request, slug, video_slug):
|
||||
video = get_object_or_404(UploadVideo, slug=video_slug)
|
||||
# video = UploadVideo.objects.get(slug=video_slug)
|
||||
title = video.title
|
||||
video.delete()
|
||||
|
||||
messages.success(request, (video.title + " has been deleted."))
|
||||
messages.success(request, f"{title} has been deleted.")
|
||||
return redirect("course_detail", slug=slug)
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Course Registration Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Course Registration
|
||||
# ########################################################
|
||||
@login_required
|
||||
@student_required
|
||||
def course_registration(request):
|
||||
student = get_object_or_404(Student, student__pk=request.user.id)
|
||||
current_semester = Semester.objects.filter(is_current_semester=True).first()
|
||||
if not current_semester:
|
||||
messages.error(request, "No active semester found.")
|
||||
return render(request, "course/course_registration.html")
|
||||
|
||||
if request.method == "POST":
|
||||
student = Student.objects.get(student__pk=request.user.id)
|
||||
ids = ()
|
||||
data = request.POST.copy()
|
||||
data.pop("csrfmiddlewaretoken", None) # remove csrf_token
|
||||
for key in data.keys():
|
||||
ids = ids + (str(key),)
|
||||
for s in range(0, len(ids)):
|
||||
course = Course.objects.get(pk=ids[s])
|
||||
obj = TakenCourse.objects.create(student=student, course=course)
|
||||
obj.save()
|
||||
course_ids = request.POST.getlist("course_ids")
|
||||
for course_id in course_ids:
|
||||
course = get_object_or_404(Course, pk=course_id)
|
||||
TakenCourse.objects.get_or_create(student=student, course=course)
|
||||
messages.success(request, "Courses registered successfully!")
|
||||
return redirect("course_registration")
|
||||
else:
|
||||
current_semester = Semester.objects.filter(is_current_semester=True).first()
|
||||
if not current_semester:
|
||||
messages.error(request, "No active semester found.")
|
||||
return render(request, "course/course_registration.html")
|
||||
|
||||
# student = Student.objects.get(student__pk=request.user.id)
|
||||
student = get_object_or_404(Student, student__id=request.user.id)
|
||||
taken_courses = TakenCourse.objects.filter(student__student__id=request.user.id)
|
||||
t = ()
|
||||
for i in taken_courses:
|
||||
t += (i.course.pk,)
|
||||
taken_course_ids = TakenCourse.objects.filter(student=student).values_list(
|
||||
"course__id", flat=True
|
||||
)
|
||||
courses = Course.objects.filter(
|
||||
program=student.program,
|
||||
level=student.level,
|
||||
semester=current_semester.semester,
|
||||
).exclude(id__in=taken_course_ids)
|
||||
|
||||
courses = (
|
||||
Course.objects.filter(
|
||||
program__pk=student.program.id,
|
||||
level=student.level,
|
||||
semester=current_semester,
|
||||
)
|
||||
.exclude(id__in=t)
|
||||
.order_by("year")
|
||||
)
|
||||
all_courses = Course.objects.filter(
|
||||
level=student.level, program__pk=student.program.id
|
||||
)
|
||||
registered_courses = Course.objects.filter(id__in=taken_course_ids)
|
||||
all_courses = Course.objects.filter(level=student.level, program=student.program)
|
||||
|
||||
no_course_is_registered = False # Check if no course is registered
|
||||
all_courses_are_registered = False
|
||||
|
||||
registered_courses = Course.objects.filter(level=student.level).filter(id__in=t)
|
||||
if (
|
||||
registered_courses.count() == 0
|
||||
): # Check if number of registered courses is 0
|
||||
no_course_is_registered = True
|
||||
|
||||
if registered_courses.count() == all_courses.count():
|
||||
all_courses_are_registered = True
|
||||
|
||||
total_first_semester_credit = 0
|
||||
total_sec_semester_credit = 0
|
||||
total_registered_credit = 0
|
||||
for i in courses:
|
||||
if i.semester == "First":
|
||||
total_first_semester_credit += int(i.credit)
|
||||
if i.semester == "Second":
|
||||
total_sec_semester_credit += int(i.credit)
|
||||
for i in registered_courses:
|
||||
total_registered_credit += int(i.credit)
|
||||
context = {
|
||||
"is_calender_on": True,
|
||||
"all_courses_are_registered": all_courses_are_registered,
|
||||
"no_course_is_registered": no_course_is_registered,
|
||||
"current_semester": current_semester,
|
||||
"courses": courses,
|
||||
"total_first_semester_credit": total_first_semester_credit,
|
||||
"total_sec_semester_credit": total_sec_semester_credit,
|
||||
"registered_courses": registered_courses,
|
||||
"total_registered_credit": total_registered_credit,
|
||||
"student": student,
|
||||
}
|
||||
return render(request, "course/course_registration.html", context)
|
||||
context = {
|
||||
"courses": courses,
|
||||
"registered_courses": registered_courses,
|
||||
"student": student,
|
||||
"current_semester": current_semester,
|
||||
"all_courses_registered": all_courses.count() == registered_courses.count(),
|
||||
}
|
||||
return render(request, "course/course_registration.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@student_required
|
||||
def course_drop(request):
|
||||
if request.method == "POST":
|
||||
student = Student.objects.get(student__pk=request.user.id)
|
||||
ids = ()
|
||||
data = request.POST.copy()
|
||||
data.pop("csrfmiddlewaretoken", None) # remove csrf_token
|
||||
for key in data.keys():
|
||||
ids = ids + (str(key),)
|
||||
for s in range(0, len(ids)):
|
||||
course = Course.objects.get(pk=ids[s])
|
||||
obj = TakenCourse.objects.get(student=student, course=course)
|
||||
obj.delete()
|
||||
messages.success(request, "Successfully Dropped!")
|
||||
student = get_object_or_404(Student, student__pk=request.user.id)
|
||||
course_ids = request.POST.getlist("course_ids")
|
||||
for course_id in course_ids:
|
||||
course = get_object_or_404(Course, pk=course_id)
|
||||
TakenCourse.objects.filter(student=student, course=course).delete()
|
||||
messages.success(request, "Courses dropped successfully!")
|
||||
return redirect("course_registration")
|
||||
|
||||
|
||||
# ########################################################
|
||||
# User Course List View
|
||||
# ########################################################
|
||||
|
||||
|
||||
@login_required
|
||||
def user_course_list(request):
|
||||
if request.user.is_lecturer:
|
||||
courses = Course.objects.filter(allocated_course__lecturer__pk=request.user.id)
|
||||
|
||||
return render(request, "course/user_course_list.html", {"courses": courses})
|
||||
|
||||
elif request.user.is_student:
|
||||
student = Student.objects.get(student__pk=request.user.id)
|
||||
taken_courses = TakenCourse.objects.filter(
|
||||
student__student__id=student.student.id
|
||||
)
|
||||
courses = Course.objects.filter(level=student.level).filter(
|
||||
program__pk=student.program.id
|
||||
)
|
||||
|
||||
if request.user.is_student:
|
||||
student = get_object_or_404(Student, student__pk=request.user.id)
|
||||
taken_courses = TakenCourse.objects.filter(student=student)
|
||||
return render(
|
||||
request,
|
||||
"course/user_course_list.html",
|
||||
{"student": student, "taken_courses": taken_courses, "courses": courses},
|
||||
{"student": student, "taken_courses": taken_courses},
|
||||
)
|
||||
|
||||
else:
|
||||
return render(request, "course/user_course_list.html")
|
||||
# For other users
|
||||
return render(request, "course/user_course_list.html")
|
||||
|
||||
@ -2,11 +2,7 @@ from django import forms
|
||||
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
|
||||
|
||||
|
||||
@ -42,9 +38,9 @@ class QuizAddForm(forms.ModelForm):
|
||||
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 +55,7 @@ class MCQuestionForm(forms.ModelForm):
|
||||
model = MCQuestion
|
||||
exclude = ()
|
||||
|
||||
|
||||
class MCQuestionFormSet(forms.BaseInlineFormSet):
|
||||
def clean(self):
|
||||
"""
|
||||
@ -69,10 +66,14 @@ class MCQuestionFormSet(forms.BaseInlineFormSet):
|
||||
super().clean()
|
||||
|
||||
# Collect non-deleted forms
|
||||
valid_forms = [form for form in self.forms if not form.cleaned_data.get('DELETE', True)]
|
||||
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_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 +81,14 @@ class MCQuestionFormSet(forms.BaseInlineFormSet):
|
||||
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 +97,7 @@ MCQuestionFormSet = inlineformset_factory(
|
||||
Choice,
|
||||
form=MCQuestionForm,
|
||||
formset=MCQuestionFormSet,
|
||||
fields=["choice", "correct"],
|
||||
fields=["choice_text", "correct"],
|
||||
can_delete=True,
|
||||
extra=5,
|
||||
)
|
||||
|
||||
130
quiz/migrations/0004_alter_essayquestion_options_and_more.py
Normal file
130
quiz/migrations/0004_alter_essayquestion_options_and_more.py
Normal file
@ -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,
|
||||
),
|
||||
]
|
||||
280
quiz/models.py
280
quiz/models.py
@ -1,23 +1,23 @@
|
||||
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 django.dispatch import receiver
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from course.models import Course
|
||||
from .utils import *
|
||||
from core.utils import unique_slug_generator
|
||||
|
||||
CHOICE_ORDER_OPTIONS = (
|
||||
("content", _("Content")),
|
||||
@ -34,97 +34,69 @@ CATEGORY_OPTIONS = (
|
||||
|
||||
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 +104,15 @@ class Quiz(models.Model):
|
||||
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 +121,18 @@ class Quiz(models.Model):
|
||||
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 +152,25 @@ class Progress(models.Model):
|
||||
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<score>\d+),(?P<possible>\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 +187,20 @@ class Progress(models.Model):
|
||||
|
||||
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 +217,7 @@ class SittingManager(models.Manager):
|
||||
|
||||
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 +226,9 @@ class SittingManager(models.Manager):
|
||||
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 +238,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 +272,14 @@ class Sitting(models.Model):
|
||||
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 +291,15 @@ class Sitting(models.Model):
|
||||
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 +307,9 @@ class Sitting(models.Model):
|
||||
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 +319,12 @@ class Sitting(models.Model):
|
||||
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 +333,14 @@ class Sitting(models.Model):
|
||||
@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 +349,10 @@ class Sitting(models.Model):
|
||||
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 +374,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 +402,76 @@ class Question(models.Model):
|
||||
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":
|
||||
return queryset.order_by("choice_text")
|
||||
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 +481,3 @@ class EssayQuestion(Question):
|
||||
|
||||
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")
|
||||
|
||||
@ -3,21 +3,20 @@ from django import template
|
||||
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
|
||||
|
||||
@ -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
|
||||
pass
|
||||
|
||||
20
quiz/urls.py
20
quiz/urls.py
@ -1,23 +1,23 @@
|
||||
from django.urls import path
|
||||
from .views import *
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("<slug>/quizzes/", quiz_list, name="quiz_index"),
|
||||
path("progress/", view=QuizUserProgressView.as_view(), name="quiz_progress"),
|
||||
path("<slug>/quizzes/", views.quiz_list, name="quiz_index"),
|
||||
path("progress/", view=views.QuizUserProgressView.as_view(), name="quiz_progress"),
|
||||
# path('marking/<int:pk>/', 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/<int:pk>/",
|
||||
view=QuizMarkingDetail.as_view(),
|
||||
view=views.QuizMarkingDetail.as_view(),
|
||||
name="quiz_marking_detail",
|
||||
),
|
||||
path("<int:pk>/<slug>/take/", view=QuizTake.as_view(), name="quiz_take"),
|
||||
path("<slug>/quiz_add/", QuizCreateView.as_view(), name="quiz_create"),
|
||||
path("<slug>/<int:pk>/add/", QuizUpdateView.as_view(), name="quiz_update"),
|
||||
path("<slug>/<int:pk>/delete/", quiz_delete, name="quiz_delete"),
|
||||
path("<int:pk>/<slug>/take/", view=views.QuizTake.as_view(), name="quiz_take"),
|
||||
path("<slug>/quiz_add/", views.QuizCreateView.as_view(), name="quiz_create"),
|
||||
path("<slug>/<int:pk>/add/", views.QuizUpdateView.as_view(), name="quiz_update"),
|
||||
path("<slug>/<int:pk>/delete/", views.quiz_delete, name="quiz_delete"),
|
||||
path(
|
||||
"mc-question/add/<slug>/<int:quiz_id>/",
|
||||
MCQuestionCreate.as_view(),
|
||||
views.MCQuestionCreate.as_view(),
|
||||
name="mc_create",
|
||||
),
|
||||
# path('mc-question/add/<int:pk>/<quiz_pk>/', MCQuestionCreate.as_view(), name='mc_create'),
|
||||
|
||||
@ -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
|
||||
336
quiz/views.py
336
quiz/views.py
@ -1,322 +1,287 @@
|
||||
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")
|
||||
self.object = form.save()
|
||||
# Save the MCQuestion instance without committing to the database yet
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
# Retrieve the Quiz instance
|
||||
quiz = get_object_or_404(Quiz, id=self.kwargs["quiz_id"])
|
||||
|
||||
# set the many-to-many relationship
|
||||
self.object.quiz.add(quiz)
|
||||
|
||||
# Save the formset (choices for the question)
|
||||
formset.instance = self.object
|
||||
formset.save()
|
||||
|
||||
if "another" in self.request.POST:
|
||||
return redirect(
|
||||
"mc_create",
|
||||
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 redirect("quiz_index", slug=self.course.slug)
|
||||
|
||||
return super(QuizTake, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
# Set self.question and self.progress here
|
||||
self.question = self.sitting.get_first_question()
|
||||
self.progress = self.sitting.progress()
|
||||
|
||||
if self.question.__class__ is EssayQuestion:
|
||||
form_class = EssayForm
|
||||
else:
|
||||
form_class = self.form_class
|
||||
|
||||
return form_class(**self.get_form_kwargs())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(QuizTake, self).get_form_kwargs()
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["question"] = self.question
|
||||
return 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 +295,39 @@ class QuizTake(FormView):
|
||||
self.sitting.add_user_answer(self.question, guess)
|
||||
self.sitting.remove_first_question()
|
||||
|
||||
# Update self.question and self.progress for the next question
|
||||
self.question = self.sitting.get_first_question()
|
||||
self.progress = self.sitting.progress()
|
||||
|
||||
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
|
||||
):
|
||||
|
||||
@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
323
result/models.py
323
result/models.py
@ -1,3 +1,6 @@
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
@ -5,35 +8,6 @@ from accounts.models import Student
|
||||
from core.models import Semester
|
||||
from course.models import Course
|
||||
|
||||
YEARS = (
|
||||
(1, "1"),
|
||||
(2, "2"),
|
||||
(3, "3"),
|
||||
(4, "4"),
|
||||
(4, "5"),
|
||||
(4, "6"),
|
||||
)
|
||||
|
||||
# LEVEL_COURSE = "Level course"
|
||||
BACHLOAR_DEGREE = "Bachloar"
|
||||
MASTER_DEGREE = "Master"
|
||||
|
||||
LEVEL = (
|
||||
# (LEVEL_COURSE, "Level course"),
|
||||
(BACHLOAR_DEGREE, "Bachloar Degree"),
|
||||
(MASTER_DEGREE, "Master Degree"),
|
||||
)
|
||||
|
||||
FIRST = "First"
|
||||
SECOND = "Second"
|
||||
THIRD = "Third"
|
||||
|
||||
SEMESTER = (
|
||||
(FIRST, "First"),
|
||||
(SECOND, "Second"),
|
||||
(THIRD, "Third"),
|
||||
)
|
||||
|
||||
A_PLUS = "A+"
|
||||
A = "A"
|
||||
A_MINUS = "A-"
|
||||
@ -47,7 +21,7 @@ D = "D"
|
||||
F = "F"
|
||||
NG = "NG"
|
||||
|
||||
GRADE = (
|
||||
GRADE_CHOICES = (
|
||||
(A_PLUS, "A+"),
|
||||
(A, "A"),
|
||||
(A_MINUS, "A-"),
|
||||
@ -65,19 +39,39 @@ GRADE = (
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
|
||||
COMMENT = (
|
||||
COMMENT_CHOICES = (
|
||||
(PASS, "PASS"),
|
||||
(FAIL, "FAIL"),
|
||||
)
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
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_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 +79,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)
|
||||
return f"{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)
|
||||
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
|
||||
def get_grade(self):
|
||||
total = self.total
|
||||
for boundary, grade in GRADE_BOUNDARIES:
|
||||
if total >= boundary:
|
||||
return grade
|
||||
return NG
|
||||
|
||||
# @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_comment(self):
|
||||
if self.grade in [F, NG]:
|
||||
return FAIL
|
||||
return PASS
|
||||
|
||||
def get_point(self, grade):
|
||||
p = 0
|
||||
# point = 0
|
||||
# for i in student:
|
||||
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
|
||||
grade_point = GRADE_POINT_MAPPING.get(self.grade, 0.0)
|
||||
return Decimal(credit) * Decimal(grade_point)
|
||||
|
||||
def calculate_gpa(self, total_credit_in_semester):
|
||||
current_semester = Semester.objects.get(is_current_semester=True)
|
||||
student = TakenCourse.objects.filter(
|
||||
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
|
||||
taken_courses = TakenCourse.objects.filter(student=self.student)
|
||||
|
||||
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
|
||||
total_points = sum(tc.point for tc in taken_courses)
|
||||
total_credits = sum(tc.course.credit for tc in taken_courses)
|
||||
|
||||
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)
|
||||
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=settings.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=settings.LEVEL_CHOICES, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Result for {self.student} - Semester: {self.semester}, Level: {self.level}"
|
||||
|
||||
@ -17,18 +17,19 @@ from reportlab.platypus import (
|
||||
)
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.enums import TA_JUSTIFY, TA_LEFT, TA_CENTER, TA_RIGHT
|
||||
from reportlab.platypus.tables import Table
|
||||
|
||||
# from reportlab.platypus.tables import Table
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
|
||||
from accounts.models import Student
|
||||
from core.models import Session, Semester
|
||||
from course.models import Course
|
||||
from accounts.models import Student
|
||||
from accounts.decorators import lecturer_required, student_required
|
||||
from .models import TakenCourse, Result, FIRST, SECOND
|
||||
from .models import TakenCourse, Result
|
||||
|
||||
|
||||
cm = 2.54
|
||||
CM = 2.54
|
||||
|
||||
|
||||
# ########################################################
|
||||
@ -130,8 +131,7 @@ def add_score_for(request, id):
|
||||
for i in courses:
|
||||
if i == courses.count():
|
||||
break
|
||||
else:
|
||||
total_credit_in_semester += int(i.credit)
|
||||
total_credit_in_semester += int(i.credit)
|
||||
score = data.getlist(
|
||||
ids[s]
|
||||
) # get list of score for current student in the loop
|
||||
@ -304,8 +304,8 @@ def result_sheet_pdf_view(request, id):
|
||||
doc = SimpleDocTemplate(
|
||||
flocation,
|
||||
rightMargin=0,
|
||||
leftMargin=6.5 * cm,
|
||||
topMargin=0.3 * cm,
|
||||
leftMargin=6.5 * CM,
|
||||
topMargin=0.3 * CM,
|
||||
bottomMargin=0,
|
||||
)
|
||||
styles = getSampleStyleSheet()
|
||||
@ -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 = [
|
||||
@ -587,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 = [
|
||||
(
|
||||
@ -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(
|
||||
@ -677,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 = [
|
||||
(
|
||||
@ -688,7 +683,7 @@ def course_registration_form(request):
|
||||
"",
|
||||
)
|
||||
]
|
||||
color = colors.black
|
||||
# color = colors.black
|
||||
count += 1
|
||||
table_body = Table(data, 1 * [1.4 * inch], 1 * [0.3 * inch])
|
||||
table_body.setStyle(
|
||||
@ -743,14 +738,14 @@ def course_registration_form(request):
|
||||
|
||||
logo = settings.STATICFILES_DIRS[0] + "/img/brand.png"
|
||||
im_logo = Image(logo, 1 * inch, 1 * inch)
|
||||
im_logo.__setattr__("_offs_x", -218)
|
||||
im_logo.__setattr__("_offs_y", 480)
|
||||
setattr(im_logo, "_offs_x", -218)
|
||||
setattr(im_logo, "_offs_y", 480)
|
||||
Story.append(im_logo)
|
||||
|
||||
picture = settings.BASE_DIR + request.user.get_picture()
|
||||
im = Image(picture, 1.0 * inch, 1.0 * inch)
|
||||
im.__setattr__("_offs_x", 218)
|
||||
im.__setattr__("_offs_y", 550)
|
||||
setattr(im, "_offs_x", 218)
|
||||
setattr(im, "_offs_y", 550)
|
||||
Story.append(im)
|
||||
|
||||
doc.build(Story)
|
||||
|
||||
@ -16,10 +16,11 @@ from course.models import Program
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
class UserFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating User instances with optional flags.
|
||||
|
||||
|
||||
Attributes:
|
||||
username (str): The generated username.
|
||||
first_name (str): The generated first name.
|
||||
@ -33,7 +34,7 @@ class UserFactory(DjangoModelFactory):
|
||||
is_parent (bool): Flag indicating if the user is a parent.
|
||||
is_dep_head (bool): Flag indicating if the user is a department head.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
@ -53,7 +54,7 @@ class UserFactory(DjangoModelFactory):
|
||||
def _create(cls, model_class: type, *args, **kwargs) -> User:
|
||||
"""
|
||||
Create a User instance with optional flags.
|
||||
|
||||
|
||||
Args:
|
||||
model_class (type): The class of the model to create.
|
||||
|
||||
@ -71,6 +72,7 @@ class UserFactory(DjangoModelFactory):
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class ProgramFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating Program instances.
|
||||
@ -90,20 +92,23 @@ class ProgramFactory(DjangoModelFactory):
|
||||
def _create(cls, model_class: type, *args, **kwargs) -> Program:
|
||||
"""
|
||||
Create a Program instance using get_or_create to avoid duplicates.
|
||||
|
||||
|
||||
Args:
|
||||
model_class (type): The class of the model to create.
|
||||
|
||||
Returns:
|
||||
Program: The created Program instance.
|
||||
"""
|
||||
program, created = Program.objects.get_or_create(title=kwargs.get("title"), defaults=kwargs)
|
||||
program, created = Program.objects.get_or_create(
|
||||
title=kwargs.get("title"), defaults=kwargs
|
||||
)
|
||||
return program
|
||||
|
||||
|
||||
class StudentFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating Student instances with associated User and Program.
|
||||
|
||||
|
||||
Attributes:
|
||||
student (User): The associated User instance.
|
||||
level (str): The level of the student.
|
||||
@ -117,10 +122,11 @@ class StudentFactory(DjangoModelFactory):
|
||||
level: str = Iterator([choice[0] for choice in LEVEL])
|
||||
program: Program = SubFactory(ProgramFactory)
|
||||
|
||||
|
||||
class ParentFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating Parent instances with associated User, Student, and Program.
|
||||
|
||||
|
||||
Attributes:
|
||||
user (User): The associated User instance.
|
||||
student (Student): The associated Student instance.
|
||||
@ -143,7 +149,9 @@ class ParentFactory(DjangoModelFactory):
|
||||
relation_ship: str = Iterator([choice[0] for choice in RELATION_SHIP])
|
||||
|
||||
|
||||
def generate_fake_accounts_data(num_programs: int, num_students: int, num_parents: int) -> None:
|
||||
def generate_fake_accounts_data(
|
||||
num_programs: int, num_students: int, num_parents: int
|
||||
) -> None:
|
||||
"""
|
||||
Generate fake data for Programs, Students, Parents, and DepartmentHeads.
|
||||
|
||||
@ -162,4 +170,3 @@ def generate_fake_accounts_data(num_programs: int, num_students: int, num_parent
|
||||
|
||||
|
||||
generate_fake_accounts_data(10, 10, 10)
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ django.setup()
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
class NewsAndEventsFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating NewsAndEvents instances.
|
||||
@ -35,6 +36,7 @@ class NewsAndEventsFactory(DjangoModelFactory):
|
||||
updated_date: timezone.datetime = fake.date_time_this_year()
|
||||
upload_time: timezone.datetime = fake.date_time_this_year()
|
||||
|
||||
|
||||
class SessionFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating Session instances.
|
||||
@ -51,7 +53,7 @@ class SessionFactory(DjangoModelFactory):
|
||||
session: str = LazyAttribute(lambda x: fake.sentence(nb_words=2))
|
||||
is_current_session: bool = fake.boolean(chance_of_getting_true=50)
|
||||
next_session_begins = LazyAttribute(lambda x: fake.future_datetime())
|
||||
|
||||
|
||||
|
||||
class SemesterFactory(DjangoModelFactory):
|
||||
"""
|
||||
@ -72,6 +74,7 @@ class SemesterFactory(DjangoModelFactory):
|
||||
session: Session = SubFactory(SessionFactory)
|
||||
next_semester_begins = LazyAttribute(lambda x: fake.future_datetime())
|
||||
|
||||
|
||||
class ActivityLogFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating ActivityLog instances.
|
||||
@ -86,7 +89,12 @@ class ActivityLogFactory(DjangoModelFactory):
|
||||
message: str = LazyAttribute(lambda x: fake.text())
|
||||
|
||||
|
||||
def generate_fake_core_data(num_news_and_events: int, num_sessions: int, num_semesters: int, num_activity_logs: int) -> None:
|
||||
def generate_fake_core_data(
|
||||
num_news_and_events: int,
|
||||
num_sessions: int,
|
||||
num_semesters: int,
|
||||
num_activity_logs: int,
|
||||
) -> None:
|
||||
"""
|
||||
Generate fake data for core models: NewsAndEvents, Session, Semester, and ActivityLog.
|
||||
|
||||
@ -97,7 +105,9 @@ def generate_fake_core_data(num_news_and_events: int, num_sessions: int, num_sem
|
||||
num_activity_logs (int): Number of ActivityLog instances to generate.
|
||||
"""
|
||||
# Generate fake NewsAndEvents instances
|
||||
news_and_events: List[NewsAndEvents] = NewsAndEventsFactory.create_batch(num_news_and_events)
|
||||
news_and_events: List[NewsAndEvents] = NewsAndEventsFactory.create_batch(
|
||||
num_news_and_events
|
||||
)
|
||||
print(f"Generated {num_news_and_events} NewsAndEvents instances.")
|
||||
|
||||
# Generate fake Session instances
|
||||
@ -109,6 +119,7 @@ def generate_fake_core_data(num_news_and_events: int, num_sessions: int, num_sem
|
||||
print(f"Generated {num_semesters} Semester instances.")
|
||||
|
||||
# Generate fake ActivityLog instances
|
||||
activity_logs: List[ActivityLog] = ActivityLogFactory.create_batch(num_activity_logs)
|
||||
activity_logs: List[ActivityLog] = ActivityLogFactory.create_batch(
|
||||
num_activity_logs
|
||||
)
|
||||
print(f"Generated {num_activity_logs} ActivityLog instances.")
|
||||
|
||||
|
||||
@ -3,7 +3,15 @@ from factory.django import DjangoModelFactory
|
||||
from factory import SubFactory, LazyAttribute, Iterator
|
||||
from faker import Faker
|
||||
|
||||
from course.models import Program, Course, CourseAllocation,Upload, UploadVideo,CourseOffer, SEMESTER
|
||||
from course.models import (
|
||||
Program,
|
||||
Course,
|
||||
CourseAllocation,
|
||||
Upload,
|
||||
UploadVideo,
|
||||
CourseOffer,
|
||||
SEMESTER,
|
||||
)
|
||||
from accounts.models import User, DepartmentHead
|
||||
from core.models import Session
|
||||
|
||||
@ -12,6 +20,7 @@ from .generate_fake_core_data import SessionFactory
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
class DepartmentHeadFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = DepartmentHead
|
||||
@ -35,6 +44,7 @@ class ProgramFactory(DjangoModelFactory):
|
||||
title: str = LazyAttribute(lambda x: fake.sentence(nb_words=3))
|
||||
summary: str = LazyAttribute(lambda x: fake.paragraph())
|
||||
|
||||
|
||||
class CourseFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating Course instances.
|
||||
@ -66,6 +76,7 @@ class CourseFactory(DjangoModelFactory):
|
||||
semester: str = Iterator([choice[0] for choice in SEMESTER])
|
||||
is_elective: bool = LazyAttribute(lambda x: fake.boolean())
|
||||
|
||||
|
||||
class CourseAllocationFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating CourseAllocation instances.
|
||||
@ -81,6 +92,7 @@ class CourseAllocationFactory(DjangoModelFactory):
|
||||
lecturer: Type[User] = SubFactory(UserFactory, is_lecturer=True)
|
||||
session: Type[Session] = SubFactory(SessionFactory)
|
||||
|
||||
|
||||
class UploadFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating Upload instances.
|
||||
@ -102,6 +114,7 @@ class UploadFactory(DjangoModelFactory):
|
||||
updated_date = fake.date_time_this_year()
|
||||
upload_time = fake.date_time_this_year()
|
||||
|
||||
|
||||
class UploadVideoFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating UploadVideo instances.
|
||||
@ -125,6 +138,7 @@ class UploadVideoFactory(DjangoModelFactory):
|
||||
summary: str = LazyAttribute(lambda x: fake.paragraph())
|
||||
timestamp = fake.date_time_this_year()
|
||||
|
||||
|
||||
class CourseOfferFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory for creating CourseOffer instances.
|
||||
@ -136,10 +150,17 @@ class CourseOfferFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = CourseOffer
|
||||
|
||||
dep_head = SubFactory(DepartmentHeadFactory)
|
||||
dep_head = SubFactory(DepartmentHeadFactory)
|
||||
|
||||
|
||||
def generate_fake_course_data(num_programs: int, num_courses: int, num_course_allocations: int, num_uploads: int, num_upload_videos: int, num_course_offers: int) -> None:
|
||||
def generate_fake_course_data(
|
||||
num_programs: int,
|
||||
num_courses: int,
|
||||
num_course_allocations: int,
|
||||
num_uploads: int,
|
||||
num_upload_videos: int,
|
||||
num_course_offers: int,
|
||||
) -> None:
|
||||
"""Generate fake data using various factories.
|
||||
|
||||
Args:
|
||||
@ -175,5 +196,4 @@ def generate_fake_course_data(num_programs: int, num_courses: int, num_course_al
|
||||
print(f"Created {len(course_offers)} course offers.")
|
||||
|
||||
|
||||
|
||||
generate_fake_course_data(10, 10, 10, 10, 10, 10)
|
||||
generate_fake_course_data(10, 10, 10, 10, 10, 10)
|
||||
|
||||
2
static/css/style.min.css
vendored
2
static/css/style.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -139,6 +139,7 @@ body {
|
||||
transition: 0.3s;
|
||||
|
||||
.nav-wrapper {
|
||||
// height: 56px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
@ -689,7 +690,7 @@ a {
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
animation: loader-bar ease-in-out 7s forwards;
|
||||
animation: loader-bar ease-in-out 3s forwards;
|
||||
}
|
||||
@keyframes loader-bar {
|
||||
0%,
|
||||
@ -1134,3 +1135,173 @@ 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);
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
{{ form.email|as_crispy_field }}
|
||||
{{ form.address|as_crispy_field }}
|
||||
{{ form.phone|as_crispy_field }}
|
||||
{{ form.gender|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mr-auto">
|
||||
<div class="col-md-6 ms-auto">
|
||||
<div class="card">
|
||||
<p class="form-title">{% trans 'Others' %}</p>
|
||||
<div class="card-body">
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
{{ form.errors }}
|
||||
|
||||
<form action="" method="POST" enctype="multipart/form-data">{% csrf_token %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
@ -29,6 +31,7 @@
|
||||
{{ form.email|as_crispy_field }}
|
||||
{{ form.first_name|as_crispy_field }}
|
||||
{{ form.last_name|as_crispy_field }}
|
||||
{{ form.gender|as_crispy_field }}
|
||||
{{ form.phone|as_crispy_field }}
|
||||
{{ form.address|as_crispy_field }}
|
||||
</div>
|
||||
|
||||
@ -56,9 +56,9 @@
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu position-fixed">
|
||||
<li><a class="dropdown-item" href="{% url 'staff_edit' pk=lecturer.pk %}"><i class="fas fa-edit"></i>{% trans 'Update' %}</a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="{% url 'profile_single' lecturer.id %}?download_pdf=1"><i class="fas fa-download"></i>{% trans 'Download PDF' %}</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'lecturer_delete' pk=lecturer.pk %}"><i class="fas fa-trash-alt"></i> {% trans 'Delete' %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'staff_edit' pk=lecturer.pk %}"><i class="unstyled me-2 fas fa-edit"></i>{% trans 'Update' %}</a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="{% url 'profile_single' lecturer.id %}?download_pdf=1"><i class="unstyled me-2 fas fa-download"></i>{% trans 'Download PDF' %}</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'lecturer_delete' pk=lecturer.pk %}"><i class="unstyled me-2 fas fa-trash-alt"></i> {% trans 'Delete' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -63,7 +63,7 @@
|
||||
{% if courses %}
|
||||
<ul>
|
||||
{% for course in courses %}
|
||||
<li><a href="{{ course.get_absolute_url }}">{{ course }}</a></li>
|
||||
<li><a href="{{ course.get_absolute_url }}" class="text-primary">{{ course }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
@ -72,7 +72,7 @@
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
<p class="fw-bold"><i class="fas fa-user"></i>{% trans 'Personal Info' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fas fa-user"></i>{% trans 'Personal Info' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'First Name:' %}</strong> {{ user.first_name|title }}</p>
|
||||
<p><strong>{% trans 'Last Name:' %}</strong> {{ user.last_name|title }}</p>
|
||||
@ -80,7 +80,7 @@
|
||||
</div>
|
||||
{% if user.is_student %}
|
||||
<hr>
|
||||
<p class="fw-bold"><i class="fas fa-graduation-cap"></i>{% trans 'Applicant Info' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fas fa-graduation-cap"></i>{% trans 'Applicant Info' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'School:' %} </strong>{% trans 'Hawas Preparatory School' %}</p>
|
||||
<p><strong>{% trans 'Level:' %} </strong>{{ level.level }}</p>
|
||||
@ -88,7 +88,7 @@
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<p class="fw-bold"><i class="fas fa-phone-square-alt"></i>{% trans 'Contact Info' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fas fa-phone-square-alt"></i>{% trans 'Contact Info' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'Email:' %}</strong> {{ user.email }}</p>
|
||||
<p><strong>{% trans 'Tel No.:' %}</strong> {{ user.phone }}</p>
|
||||
@ -96,7 +96,7 @@
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<p class="fw-bold"><i class="fa fa-calendar-day"></i>{% trans 'Important Dates' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fa fa-calendar-day"></i>{% trans 'Important Dates' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'Last login:' %}</strong> {{ user.last_login }}</p>
|
||||
{% if current_semester and current_session %}
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
{% if courses %}
|
||||
<ul>
|
||||
{% for course in courses %}
|
||||
<li><a href="{{ course.get_absolute_url }}">{{ course }}</a></li>
|
||||
<li><a href="{{ course.get_absolute_url }}" class="text-primary">{{ course }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
@ -85,7 +85,7 @@
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
<p class="fw-bold"><i class="fas fa-user"></i> {% trans 'Personal Info' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fas fa-user"></i> {% trans 'Personal Info' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'First Name' %}:</strong> {{ user.first_name|title }}</p>
|
||||
<p><strong>{% trans 'Last Name' %}:</strong> {{ user.last_name|title }}</p>
|
||||
@ -93,7 +93,7 @@
|
||||
</div>
|
||||
{% if user.is_student %}
|
||||
<hr>
|
||||
<p class="fw-bold"><i class="fas fa-graduation-cap"></i> {% trans 'Applicant Info' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fas fa-graduation-cap"></i> {% trans 'Applicant Info' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'School' %}: </strong>Unity College</p>
|
||||
<p><strong>{% trans 'Level' %}: </strong>{{ level.level }}</p>
|
||||
@ -102,7 +102,7 @@
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<p class="fw-bold"><i class="fas fa-phone-square-alt"></i>{% trans 'Contact Info' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fas fa-phone-square-alt"></i>{% trans 'Contact Info' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'Email' %}:</strong> {{ user.email }}</p>
|
||||
<p><strong>{% trans 'Tel No.' %}:</strong> {{ user.phone }}</p>
|
||||
@ -110,7 +110,7 @@
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<p class="fw-bold"><i class="fa fa-calendar-day"></i>{% trans 'Important Dates' %}</p>
|
||||
<p class="fw-bold"><i class="me-2 fa fa-calendar-day"></i>{% trans 'Important Dates' %}</p>
|
||||
<div class="dashboard-description">
|
||||
<p><strong>{% trans 'Last login' %}:</strong> {{ user.last_login }}</p>
|
||||
{% if current_semester and current_session %}
|
||||
|
||||
@ -58,9 +58,9 @@
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu position-fixed">
|
||||
<li><a class="dropdown-item" href="{% url 'student_edit' student.student.pk %}"><i class="fas fa-edit"></i>{% trans 'Update' %}</a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="{% url 'profile_single' student.student.id %}?download_pdf=1"><i class="fas fa-download"></i>{% trans 'Download PDF' %}</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'student_delete' student.pk %}"><i class="fas fa-trash-alt"></i>{% trans 'Delete' %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'student_edit' student.student.pk %}"><i class="unstyled me-2 fas fa-edit"></i>{% trans 'Update' %}</a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="{% url 'profile_single' student.student.id %}?download_pdf=1"><i class="unstyled me-2 fas fa-download"></i>{% trans 'Download PDF' %}</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'student_delete' student.pk %}"><i class="unstyled me-2 fas fa-trash-alt"></i>{% trans 'Delete' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<body>
|
||||
{% block sidebar %}
|
||||
{% include 'aside.html' %}
|
||||
{% include 'sidebar.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
@ -5,30 +5,6 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans 'Home' %}</li>
|
||||
@ -73,9 +49,9 @@
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{% url 'edit_post' pk=item.id %}"><i
|
||||
class="fas fa-pencil-alt"></i>{% trans 'Edit' %}</a>
|
||||
class="unstyled me-2 fas fa-pencil-alt"></i>{% trans 'Edit' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'delete_post' pk=item.id %}"><i
|
||||
class="fas fa-trash-alt"></i>{% trans 'Delete' %}</a>
|
||||
class="unstyled me-2 fas fa-trash-alt"></i>{% trans 'Delete' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -19,19 +19,7 @@
|
||||
|
||||
<div class="title-1"><i class="fas fa-calendar-alt"></i>{% trans 'Semester List' %}</div>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="table-responsive table-shadow p-0 mt-5">
|
||||
<table class="table">
|
||||
|
||||
@ -14,19 +14,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mx-auto">
|
||||
|
||||
@ -19,19 +19,7 @@
|
||||
|
||||
<div class="title-1"><i class="fas fa-calendar-week"></i>{% trans 'Session List' %}</div>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="table-responsive table-shadow p-0 mt-5">
|
||||
<table class="table">
|
||||
|
||||
@ -14,19 +14,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mx-auto">
|
||||
|
||||
@ -107,7 +107,7 @@
|
||||
<span class="text-danger">
|
||||
{% trans 'No video Uploaded.' %}
|
||||
{% if request.user.is_superuser or request.user.is_lecturer %}
|
||||
<a href="{% url 'upload_video' course.slug %}">
|
||||
<a href="{% url 'upload_video' course.slug %}" class="text-primary">
|
||||
<i class="primary" style="font-size: 22px;">
|
||||
{% trans 'Upload now.' %}
|
||||
</i>
|
||||
@ -184,7 +184,7 @@
|
||||
<span class="text-danger">
|
||||
{% trans 'No File Uploaded.' %}
|
||||
{% if request.user.is_superuser or request.user.is_lecturer %}
|
||||
<a href="{% url 'upload_file_view' course.slug %}">
|
||||
<a href="{% url 'upload_file_view' course.slug %}" class="text-primary">
|
||||
<i class="primary" style="font-size: 22px;">
|
||||
{% trans 'Upload now.' %}
|
||||
</i>
|
||||
|
||||
@ -53,8 +53,8 @@
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu position-fixed">
|
||||
<li><a class="dropdown-item" href="{% url 'edit_program' pk=program.pk %}"><i class="fas fa-edit"></i>{% trans 'Update' %}</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'program_delete' pk=program.pk %}"><i class="fas fa-trash-alt"></i>{% trans 'Delete' %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'edit_program' pk=program.pk %}"><i class="unstyled me-2 fas fa-edit"></i>{% trans 'Update' %}</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'program_delete' pk=program.pk %}"><i class="unstyled me-2 fas fa-trash-alt"></i>{% trans 'Delete' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -79,10 +79,10 @@
|
||||
</button>
|
||||
<div class="dropdown-menu position-fixed">
|
||||
<a class="dropdown-item" href="{% url 'edit_course' slug=course.slug %}">
|
||||
<i class="fas fa-pencil-alt"></i> {% trans 'Edit' %}
|
||||
<i class="unstyled me-2 fas fa-pencil-alt"></i> {% trans 'Edit' %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url 'delete_course' slug=course.slug %}">
|
||||
<i class="fas fa-trash-alt"></i> {% trans 'Delete' %}
|
||||
<i class="unstyled me-2 fas fa-trash-alt"></i> {% trans 'Delete' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,7 +95,7 @@
|
||||
<span class="text-danger">
|
||||
{% trans 'No course for this progrm.' %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url 'course_add' pk=program.pk %}">
|
||||
<a href="{% url 'course_add' pk=program.pk %}" class="text-primary">
|
||||
<i class="primary" style="font-size: 22px;">
|
||||
{% trans 'Add one now.' %}
|
||||
</i>
|
||||
|
||||
@ -23,19 +23,7 @@
|
||||
<div class="title-1">{% trans 'My Courses' %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
{% if request.user.is_student %}
|
||||
<div class="table-responsive p-3 mt-3">
|
||||
|
||||
@ -1,36 +1,5 @@
|
||||
{% block content %}
|
||||
{% load i18n %}
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<p class="title-1">{% trans 'Lecturers' %}</p>
|
||||
|
||||
|
||||
@ -1,33 +1,6 @@
|
||||
{% block content %}
|
||||
{% load i18n %}
|
||||
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="row">
|
||||
<div class="card-header">
|
||||
|
||||
@ -1,36 +1,5 @@
|
||||
{% block content %}
|
||||
{% load i18n %}
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<p class="title-1">{% trans 'Students' %}</p>
|
||||
|
||||
|
||||
@ -1,19 +1,12 @@
|
||||
{% load i18n %}
|
||||
{% if previous.answers %}
|
||||
|
||||
{% if user_was_incorrect %}
|
||||
<div class="alert alert-error">
|
||||
<strong>{% trans "You answered the above question incorrectly" %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<tbody>
|
||||
{% for answer in previous.answers %}
|
||||
{% if answer.correct %}
|
||||
<tr class="success">
|
||||
<td>{{ answer }}</td>
|
||||
<td><strong>{% trans "This is the correct answer" %}</strong></td>
|
||||
<td class="text-success"><strong>{% trans "This is the correct answer" %}</strong></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
@ -30,4 +23,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if user_was_incorrect %}
|
||||
<div class="text-danger">
|
||||
<strong>{% trans "You answered the above question incorrectly" %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@ -28,7 +28,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="container">
|
||||
<div class="info-text bg-orange mb-3">{{ quizQuestions }} {% trans 'question added' %}</div>
|
||||
<div class="mb-3 bg-secondary text-light py-1 px-3">{{ quiz_questions_count }} {% trans 'question added' %}</div>
|
||||
|
||||
<form action="#" method="POST">{% csrf_token %}
|
||||
{% if form.errors %}<p class="alert alert-danger">{% trans 'Correct the error(s) below.' %}</p>{% endif %}
|
||||
@ -52,7 +52,7 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ fs.correct }} <small class="ms-1">{% trans 'Correct' %}</small></span>
|
||||
</div>
|
||||
{{ fs.choice }}
|
||||
{{ fs.choice_text }}
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ fs.DELETE }} <small class="ms-1">{% trans 'Delete' %}</small></span>
|
||||
</div>
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<p class="title-1"><i class="fas fa-record-vinyl"></i>{% trans 'Quiz Progress Rec' %}</p>
|
||||
|
||||
{% if cat_scores %}
|
||||
|
||||
<div class="header-title text-center">{% trans "Question Category Scores" %}</div>
|
||||
@ -53,7 +55,11 @@
|
||||
|
||||
<div class="header-title-xl">{% trans "Previous exam papers" %}</div>
|
||||
<p class="lead fw-bold">
|
||||
{% 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 %}
|
||||
</p>
|
||||
<div class="text-light bg-secondary mb-2 p-1">{% trans 'Total complete exams:' %} {{ exams_counter }}</div>
|
||||
<div class="table-responsive">
|
||||
@ -89,7 +95,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not cat_scores and not exams %}
|
||||
<div class="col-12 p-4 text-center"><h3><i class="far fa-frown"></i></h3> {% trans 'No recordes yet. Try to do some quizzes in your course.' %}</div>
|
||||
<h4 class="text-center mt-5 py-5 text-muted">
|
||||
<i class="fa-regular fa-folder-open me-2"></i>{% trans 'Progress records will appear here' %}
|
||||
</h4>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@ -33,7 +33,7 @@
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% endif %}
|
||||
<p><small>
|
||||
<p class="mb-0"><small>
|
||||
{% trans "Your answer was" %} </small>
|
||||
<strong>
|
||||
{{ previous.previous_outcome|yesno:"correct,incorrect" }}
|
||||
@ -44,20 +44,13 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% if previous.answers %}
|
||||
|
||||
{% if user_was_incorrect %}
|
||||
<div class="alert alert-error">
|
||||
<strong>{% trans "You answered the above question incorrectly" %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<tbody>
|
||||
{% for answer in previous.answers %}
|
||||
{% if answer.correct %}
|
||||
<tr class="success">
|
||||
<td>{{ answer }}</td>
|
||||
<td><strong>{% trans "This is the correct answer" %}</strong></td>
|
||||
<td class="text-success"><strong>{% trans "This is the correct answer" %}</strong></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
@ -74,6 +67,13 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if user_was_incorrect %}
|
||||
<div class="text-danger">
|
||||
<strong>{% trans "You answered the above question incorrectly" %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<style>.bg-light-warning{background-color: rgb(252, 217, 111) !important;}</style>
|
||||
|
||||
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
@ -66,10 +65,10 @@
|
||||
<button class="btn btn-sm p-0 ms-2" type="button" data-bs-toggle="dropdown"><i class="fas fa-ellipsis-v m-0"></i></button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdown01">
|
||||
<div class="dropdown-item">
|
||||
<a href="{% url 'quiz_update' slug=course.slug pk=quiz.id %}" class="update"><i class="fas fa-pencil-alt"></i>{% trans 'Edit' %}</a>
|
||||
<a href="{% url 'quiz_update' slug=course.slug pk=quiz.id %}" class="update"><i class="unstyled me-2 fas fa-pencil-alt"></i>{% trans 'Edit' %}</a>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="{% url 'quiz_delete' slug=course.slug pk=quiz.id %}" class="delete"><i class="fas fa-trash-alt"></i>{% trans 'Delete' %}</a>
|
||||
<a href="{% url 'quiz_delete' slug=course.slug pk=quiz.id %}" class="delete"><i class="unstyled me-2 fas fa-trash-alt"></i>{% trans 'Delete' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -97,6 +96,6 @@
|
||||
document.getElementById('progress-card').style.display = 'none';
|
||||
document.getElementById('progress-main').style.display = 'block';
|
||||
clearInterval(timer)
|
||||
}, 8000);
|
||||
}, 4000);
|
||||
</script>
|
||||
{% endblock js %}
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
<td>
|
||||
<form action="" method="POST">{% csrf_token %}
|
||||
<input type="hidden" name="qid" value="{{ question.id }}">
|
||||
<button type="submit" class="btn btn-warning">{% trans "Toggle whether correct" %}</button>
|
||||
<button type="submit" class="btn btn-sm btn-secondary">{% trans "Toggle whether correct" %}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@ -30,14 +30,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>#progress-main{display: none;}</style>
|
||||
|
||||
<div class="container" id="progress-main">
|
||||
{% if previous.answers %}
|
||||
<div class="card bg-white p-3">
|
||||
<p class="muted"><small>{% trans "The previous question" %}:</small></p>
|
||||
<p>{{ previous.previous_question }}</p>
|
||||
<p>Your answer was
|
||||
<p class="mb-0">Your answer was
|
||||
<strong>
|
||||
{{ previous.previous_outcome|yesno:"correct,incorrect" }}
|
||||
</strong>
|
||||
@ -45,20 +43,13 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% if previous.answers %}
|
||||
|
||||
{% if user_was_incorrect %}
|
||||
<div class="alert alert-error">
|
||||
<strong>{% trans "You answered the above question incorrectly" %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<tbody>
|
||||
{% for answer in previous.answers %}
|
||||
{% if answer.correct %}
|
||||
<tr class="success">
|
||||
<td>{{ answer }}</td>
|
||||
<td><strong>{% trans "This is the correct answer" %}</strong></td>
|
||||
<td class="text-success"><strong>{% trans "This is the correct answer" %}</strong></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
@ -75,6 +66,13 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if user_was_incorrect %}
|
||||
<div class="text-danger">
|
||||
<strong>{% trans "You answered the above question incorrectly" %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<p><strong>{% trans "Explanation" %}:</strong></p>
|
||||
@ -161,7 +159,7 @@
|
||||
{% correct_answer_for_all question %}
|
||||
|
||||
{% if question.user_answer %}
|
||||
<p>{% trans "Your answer" %}: {{ question|answer_choice_to_string:question.user_answer }}</p>
|
||||
<p><span class="bg-secondary px-3 py-1 text-light">{% trans "Your answer" %}: {{ question|answer_choice_to_string:question.user_answer }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
@ -11,8 +11,6 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="title-1"><i class="fas fa-calendar-alt"></i>{% trans "List of complete exams" %}</div>
|
||||
|
||||
{% for student in students %}<h3>{{ student.student.user.get_full_name }}</h3>{% endfor %}
|
||||
@ -62,5 +60,4 @@
|
||||
{% else %}
|
||||
<p class="p-3 bg-light">{% trans "No completed exams for you" %}.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -4,19 +4,7 @@
|
||||
{% load crispy_forms_tags %}
|
||||
{% block content %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div id="login">
|
||||
<h3 class="login-title">{% trans 'Confirm New Password' %}</h3>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
<div class="blue-gradient text-light p-3 mb-5">
|
||||
<h1 class="lead my-0">
|
||||
<i class="fas fa-lock mr-2"></i>{% trans 'Create Your Account' %}
|
||||
<i class="fas fa-lock ms-2"></i>{% trans 'Create Your Account' %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@ -11,19 +11,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="title-1"><i class="fa fa-spell-check"></i>{% trans 'Assesment Results' %}</div>
|
||||
<p>{{ student.level }} {% trans 'Result' %}</p>
|
||||
|
||||
@ -11,19 +11,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="title-1"><i class="fas fa-table"></i>{% trans 'Grade Results' %}</div>
|
||||
<p>{{ student.level }} {% trans 'Result' %}</p>
|
||||
|
||||
@ -13,32 +13,6 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.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: .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);
|
||||
background: #6cbd45;
|
||||
}
|
||||
.class-item:hover h4 a {
|
||||
color: #fff; }
|
||||
.class-item:hover p, .class-item:hover span {
|
||||
color: rgb(158, 239, 119);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card p-3" style="box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); border-radius: 10px;">
|
||||
<h5 class="text-muted m-0">{{ count }} {% trans 'result' %}{{ count|pluralize }} {% trans 'for' %} <b><em class="text-orange"> {{ query }}</em></b></h5>
|
||||
<hr>
|
||||
@ -46,7 +20,7 @@
|
||||
{% with object|class_name as klass %}
|
||||
{% if klass == "Program" %}
|
||||
<div class="session-wrapper">
|
||||
<div class="session"><div class="info-text bg-orange">{% trans 'Program' %}</div></div>
|
||||
<div class="session"><div class="info-text bg-secondary text-light px-2 rounded-pill">{% trans 'Program' %}</div></div>
|
||||
</div>
|
||||
<div class="col-12 class-item">
|
||||
<!-- <p><b>Program of</b> {{ object }}</p> -->
|
||||
@ -56,7 +30,7 @@
|
||||
|
||||
{% elif klass == "Course" %}
|
||||
<div class="session-wrapper">
|
||||
<div class="session"><div class="info-text bg-orange">{% trans 'Course' %}</div></div>
|
||||
<div class="session"><div class="info-text bg-secondary text-light px-2 rounded-pill">{% trans 'Course' %}</div></div>
|
||||
</div>
|
||||
<div class="col-12 class-item">
|
||||
<p><b>{% trans 'Program of' %}</b> {{ object.program }}</p>
|
||||
@ -66,7 +40,7 @@
|
||||
|
||||
{% elif klass == "NewsAndEvents" %}
|
||||
<div class="session-wrapper">
|
||||
<div class="session"><div class="info-text bg-orange">{% trans 'News And Events' %}</div></div>
|
||||
<div class="session"><div class="info-text bg-secondary text-light px-2 rounded-pill">{% trans 'News And Events' %}</div></div>
|
||||
</div>
|
||||
<div class="col-12 class-item">
|
||||
<p><b>{% trans 'Date:' %} </b> {{ object.updated_date|timesince }} ago</p>
|
||||
@ -76,7 +50,7 @@
|
||||
|
||||
{% elif klass == "Quiz" %}
|
||||
<div class="session-wrapper">
|
||||
<div class="session"><div class="info-text bg-orange">{% trans 'Quiz' %}</div></div>
|
||||
<div class="session"><div class="info-text bg-secondary text-light px-2 rounded-pill">{% trans 'Quiz' %}</div></div>
|
||||
</div>
|
||||
<div class="col-12 class-item">
|
||||
<p>{{ object.category }} {% trans 'quiz' %}, <b>{% trans 'Course:' %}</b> {{ object.course }}</p>
|
||||
@ -86,7 +60,7 @@
|
||||
|
||||
{% else %}
|
||||
<div class="session-wrapper">
|
||||
<div class="session"><div class="info-text bg-orange">{% trans 'Program' %}</div></div>
|
||||
<div class="session"><div class="info-text bg-secondary text-light px-2 rounded-pill">{% trans 'Program' %}</div></div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-8 offset-lg-4">
|
||||
<a href="{{ object.get_absolute_url }}" class="class-item d-flex">{{ object }} | {{ object|class_name }}</a>
|
||||
@ -123,7 +97,7 @@
|
||||
<ul class="pl-3">
|
||||
<li>{% trans 'Program' %} <span class="text-orange">></span> {% trans 'Title or Description' %}</li>
|
||||
<li>{% trans 'Course' %} <span class="text-orange">></span>{% trans 'Title, Code or Description' %}</li>
|
||||
<li>{% trans 'News And Events' %} <span class="text-orange">></span> {% trans 'Title, Description or just by typing "news" or "event %}li>
|
||||
<li>{% trans 'News And Events' %} <span class="text-orange">></span> {% trans 'Title, Description or just by typing news or event' %}li>
|
||||
<li>{% trans 'Quiz' %} <span class="text-orange">></span>{% trans 'Title, Description or Category(practice, assignment and exam)' %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -16,35 +16,24 @@
|
||||
<div class="title-1"><i class="fas fa-user-tie"></i>{% trans 'Admin Panel' %}</div>
|
||||
<br>
|
||||
<br>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'snippets/messages.html' %}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="border-bottom">
|
||||
{% trans 'Manage' %}<a class="link" href="{% url 'lecturer_list' %}">{% trans 'Lecturers' %} »</a>
|
||||
{% trans 'Manage' %}<a class="link" href="{% url 'lecturer_list' %}"> {% trans 'Lecturers' %} »</a>
|
||||
<p class="text-muted">CRUD (Create, Retrieve, Update & Delete) lecturers</p>
|
||||
</div>
|
||||
|
||||
<div class="border-bottom mt-3">
|
||||
{% trans 'Manage' %}<a class="link" href="{% url 'student_list' %}">{% trans 'Students' %} »</a>
|
||||
{% trans 'Manage' %}<a class="link" href="{% url 'student_list' %}"> {% trans 'Students' %} »</a>
|
||||
<p class="text-muted">CRUD (Create, Retrieve, Update & Delete) {% trans 'students' %}</p>
|
||||
</div>
|
||||
|
||||
<div class="border-bottom mt-3">
|
||||
{% trans 'Manage' %}<a class="link" href="{% url 'session_list' %}">{% trans 'Session' %} »</a>
|
||||
{% trans 'Manage' %}<a class="link" href="{% url 'session_list' %}"> {% trans 'Session' %} »</a>
|
||||
<p class="text-muted">CRUD (Create, Retrieve, Update & Delete) {% trans 'sessions' %}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<style>
|
||||
.top-side {
|
||||
background-size: cover;
|
||||
background-position: top center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="side-nav">
|
||||
<div class="main-menu">
|
||||
<div class="top-side text-center py-4" style="background-image: url({% static 'img/dotted.jpg' %});">
|
||||
@ -115,7 +108,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
<br />
|
||||
<p class="ml-3">→ Others</p>
|
||||
<p class="ms-3 text-secondary">→ Others</p>
|
||||
<li class="{% if request.path == ep %}active{% endif %}">
|
||||
<a href="{% url 'edit_profile' %}"><i class="fas fa-cogs"></i>{% trans 'Account Setting' %}</a>
|
||||
</li>
|
||||
@ -18,14 +18,6 @@
|
||||
<p class="title-1">{{ video.title }}</p>
|
||||
<br><br>
|
||||
|
||||
<style>
|
||||
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);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="col-md-10 mx-auto d-block">
|
||||
<div class=""><video src="{{ video.video.url }}" controls ></video></div>
|
||||
<p><i class="fas fa-calendar"></i> {{ video.timestamp|timesince }} {% trans 'ago' %}</p>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user