diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6c0712..c3d196a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,9 @@ default_language_version: python: python3.9 -exclude: | - (?x)^( - static/.* - )$ - repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -16,42 +11,47 @@ repos: - id: check-merge-conflict - id: check-symlinks - id: check-toml + - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + # - repo: https://github.com/asottile/pyupgrade + # rev: v3.17.0 + # hooks: + # - id: pyupgrade + # args: [--py312-plus] + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.21.0 + hooks: + - id: django-upgrade + args: [--target-version, "4.2"] - repo: https://github.com/rtts/djhtml - rev: '3.0.5' + rev: '3.0.6' hooks: - id: djhtml - entry: djhtml --tabwidth 4 - alias: autoformat - - id: djcss + entry: djhtml --tabwidth 2 alias: autoformat - - id: djjs - alias: autoformat - - repo: https://github.com/adamchainz/django-upgrade - rev: 1.13.0 + # - id: djcss + # alias: autoformat + # - id: djjs + # alias: autoformat + - repo: https://github.com/adamchainz/djade-pre-commit + rev: "1.2.0" hooks: - - id: django-upgrade - args: [--target-version, "4.2"] - alias: autoformat - - repo: https://github.com/psf/black - rev: 23.1.0 + - id: djade + args: [--target-version, "4.2"] + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.14.0 hooks: - - id: black - alias: autoformat + - id: pretty-format-toml + args: [--autofix] - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 + rev: 1.18.0 hooks: - id: blacken-docs - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.256' - hooks: - - id: ruff alias: autoformat - args: [--fix] - # - repo: https://github.com/codespell-project/codespell - # rev: v2.2.2 - # hooks: - # - id: codespell - # args: [--write-changes] - # alias: autoformat diff --git a/fly.toml b/fly.toml index c6ea7d4..bab301c 100644 --- a/fly.toml +++ b/fly.toml @@ -7,45 +7,46 @@ primary_region = "den" processes = [] [deploy] - release_command = "python manage.py migrate --noinput" - -[http_service] - auto_start_machines = true - auto_stop_machines = true - force_https = false - internal_port = 8000 - min_machines_running = 0 - processes = ["app"] +release_command = "python manage.py migrate --noinput" [env] - PORT = "8000" +PORT = "8000" [experimental] - allowed_public_ports = [] - auto_rollback = true +allowed_public_ports = [] +auto_rollback = true + +[http_service] +auto_start_machines = true +auto_stop_machines = true +force_https = false +internal_port = 8000 +min_machines_running = 0 +processes = ["app"] [[services]] - http_checks = [] - internal_port = 8000 - processes = ["app"] - protocol = "tcp" - script_checks = [] - [services.concurrency] - hard_limit = 25 - soft_limit = 20 - type = "connections" - - [[services.ports]] - force_https = true - handlers = ["http"] - port = 80 - - [[services.ports]] - handlers = ["tls", "http"] - port = 443 - - [[services.tcp_checks]] - grace_period = "1s" - interval = "15s" - restart_limit = 0 - timeout = "2s" +http_checks = [] +internal_port = 8000 +processes = ["app"] +protocol = "tcp" +script_checks = [] + +[services.concurrency] +hard_limit = 25 +soft_limit = 20 +type = "connections" + +[[services.ports]] +force_https = true +handlers = ["http"] +port = 80 + +[[services.ports]] +handlers = ["tls", "http"] +port = 443 + +[[services.tcp_checks]] +grace_period = "1s" +interval = "15s" +restart_limit = 0 +timeout = "2s" diff --git a/grants/forms.py b/grants/forms.py index a7d8d53..4bd806a 100644 --- a/grants/forms.py +++ b/grants/forms.py @@ -12,11 +12,7 @@ class Meta: def clean_type(self): type = self.cleaned_data["type"] - if ( - self.instance - and type != self.instance.type - and self.instance.answers.exists() - ): + if self.instance and type != self.instance.type and self.instance.answers.exists(): raise forms.ValidationError("Cannot change once this question has answers") return type @@ -32,9 +28,7 @@ def __init__(self, program, *args, **kwargs): def clean_email(self): email = self.cleaned_data["email"] if self.program.applicants.filter(email=email).exists(): - raise forms.ValidationError( - "An application with that email address has already been submitted." - ) + raise forms.ValidationError("An application with that email address has already been submitted.") return email @@ -86,7 +80,5 @@ def __init__(self, applicant, *args, **kwargs): def clean_resource(self): resource = self.cleaned_data["resource"] if self.applicant.allocations.filter(resource=resource).exists(): - raise forms.ValidationError( - "That resource is already allocated. Delete it if you wish to change it." - ) + raise forms.ValidationError("That resource is already allocated. Delete it if you wish to change it.") return resource diff --git a/grants/models.py b/grants/models.py index 3ce4ce6..b70404c 100644 --- a/grants/models.py +++ b/grants/models.py @@ -59,9 +59,7 @@ class Resource(models.Model): ("accomodation", "Accomodation"), ] - program = models.ForeignKey( - Program, related_name="resources", on_delete=models.CASCADE - ) + program = models.ForeignKey(Program, related_name="resources", on_delete=models.CASCADE) name = models.CharField(max_length=100) type = models.CharField(max_length=50, choices=TYPE_CHOICES) amount = models.PositiveIntegerField() @@ -99,9 +97,7 @@ class Question(models.Model): ("integer", "Integer value"), ] - program = models.ForeignKey( - Program, related_name="questions", on_delete=models.CASCADE - ) + program = models.ForeignKey(Program, related_name="questions", on_delete=models.CASCADE) type = models.CharField(max_length=50, choices=TYPE_CHOICES) question = models.TextField() required = models.BooleanField(default=False) @@ -122,9 +118,7 @@ class Applicant(models.Model): Someone applying for a grant. """ - program = models.ForeignKey( - Program, related_name="applicants", on_delete=models.CASCADE - ) + program = models.ForeignKey(Program, related_name="applicants", on_delete=models.CASCADE) name = models.TextField() email = models.EmailField() @@ -166,12 +160,8 @@ class Allocation(models.Model): An allocation of some Resources to an Applicant. """ - applicant = models.ForeignKey( - Applicant, related_name="allocations", on_delete=models.CASCADE - ) - resource = models.ForeignKey( - Resource, related_name="allocations", on_delete=models.CASCADE - ) + applicant = models.ForeignKey(Applicant, related_name="allocations", on_delete=models.CASCADE) + resource = models.ForeignKey(Resource, related_name="allocations", on_delete=models.CASCADE) amount = models.PositiveIntegerField() class Meta: @@ -185,12 +175,8 @@ class Answer(models.Model): An applicant's answer to a question. """ - applicant = models.ForeignKey( - Applicant, related_name="answers", on_delete=models.CASCADE - ) - question = models.ForeignKey( - Question, related_name="answers", on_delete=models.CASCADE - ) + applicant = models.ForeignKey(Applicant, related_name="answers", on_delete=models.CASCADE) + question = models.ForeignKey(Question, related_name="answers", on_delete=models.CASCADE) answer = models.TextField() class Meta: @@ -204,15 +190,9 @@ class Score(models.Model): A score and optional comment on an applicant by a user. """ - applicant = models.ForeignKey( - Applicant, related_name="scores", on_delete=models.CASCADE - ) - user = models.ForeignKey( - "users.User", related_name="scores", on_delete=models.CASCADE - ) - score = models.FloatField( - blank=True, null=True, help_text="From 1 (terrible) to 5 (excellent)" - ) + applicant = models.ForeignKey(Applicant, related_name="scores", on_delete=models.CASCADE) + user = models.ForeignKey("users.User", related_name="scores", on_delete=models.CASCADE) + score = models.FloatField(blank=True, null=True, help_text="From 1 (terrible) to 5 (excellent)") comment = models.TextField( blank=True, null=True, diff --git a/grants/views/bulk_load.py b/grants/views/bulk_load.py index 1a0ac2a..2338ec3 100644 --- a/grants/views/bulk_load.py +++ b/grants/views/bulk_load.py @@ -27,16 +27,12 @@ def post(self, request): # If there's a CSV, load it into the database, otherwise retrieve # the one we stored there before. if "csv" in request.FILES: - csv_obj = UploadedCSV.objects.create( - csv=request.FILES["csv"].read().decode() - ) + csv_obj = UploadedCSV.objects.create(csv=request.FILES["csv"].read().decode()) else: csv_obj = UploadedCSV.objects.get(pk=request.POST["csv_id"]) # We always get a CSV file - parse it. - reader = csv.reader( - [x + "\n" for x in csv_obj.csv.split("\n") if len(x.strip())] - ) + reader = csv.reader([x + "\n" for x in csv_obj.csv.split("\n") if len(x.strip())]) rows = list(reader) headers = rows[0] @@ -45,9 +41,7 @@ def post(self, request): fields = collections.OrderedDict( ( name, - forms.ChoiceField( - choices=column_choices, required=required, label=label - ), + forms.ChoiceField(choices=column_choices, required=required, label=label), ) for name, required, label in self.get_targets() ) @@ -59,11 +53,7 @@ def post(self, request): # Save and import! errors = [] successful = 0 - target_map = { - name: int(value) - for name, value in form.cleaned_data.items() - if name != "csv_id" and value - } + target_map = {name: int(value) for name, value in form.cleaned_data.items() if name != "csv_id" and value} for i, row in enumerate(rows[1:]): try: with atomic(): @@ -120,9 +110,7 @@ def process_row(self, row, target_map): if self.program.duplicate_emails: applicant = None else: - applicant = Applicant.objects.filter( - program=self.program, email=row[target_map["email"]] - ).first() + applicant = Applicant.objects.filter(program=self.program, email=row[target_map["email"]]).first() if not applicant: applicant = Applicant( program=self.program, @@ -139,9 +127,7 @@ def process_row(self, row, target_map): if target_map.get("timestamp", None): for time_format in self.time_formats: try: - applicant.applied = datetime.datetime.strptime( - row[target_map["timestamp"]], time_format - ) + applicant.applied = datetime.datetime.strptime(row[target_map["timestamp"]], time_format) except ValueError: pass applicant.save() @@ -152,10 +138,7 @@ def process_row(self, row, target_map): question = self.program.questions.get(pk=key.lstrip("q")) if question.type == "boolean": answer = str( - not any( - (raw_answer.lower().strip() == no_word) - for no_word in ("no", "false", "off", "", "0") - ) + not any((raw_answer.lower().strip() == no_word) for no_word in ("no", "false", "off", "", "0")) ) elif question.type == "integer": if not raw_answer.strip(): @@ -165,14 +148,11 @@ def process_row(self, row, target_map): answer = str(int(raw_answer.strip())) except ValueError: raise ValueError( - "Invalid integer value for question %s: %s" - % (question.question, raw_answer) + "Invalid integer value for question %s: %s" % (question.question, raw_answer) ) else: answer = raw_answer - answer_obj = Answer.objects.filter( - applicant=applicant, question=question - ).first() + answer_obj = Answer.objects.filter(applicant=applicant, question=question).first() if not answer_obj: answer_obj = Answer( applicant=applicant, @@ -199,9 +179,7 @@ def get_targets(self): def process_row(self, row, target_map): applicant = Applicant.objects.get(email=row[target_map["email"]]) - score = Score.objects.get_or_create( - applicant=applicant, user=self.request.user - )[0] + score = Score.objects.get_or_create(applicant=applicant, user=self.request.user)[0] score_value = row[target_map["score"]] try: score.score = float(score_value) diff --git a/grants/views/program.py b/grants/views/program.py index 747571f..2743a68 100644 --- a/grants/views/program.py +++ b/grants/views/program.py @@ -20,9 +20,7 @@ def index(request): request, "index.html", { - "accessible_programs": Program.objects.filter( - users__pk=request.user.pk - ).order_by("name"), + "accessible_programs": Program.objects.filter(users__pk=request.user.pk).order_by("name"), }, ) @@ -62,9 +60,7 @@ def get_context_data(self): user.num_votes = user.scores.filter(applicant__program=self.program).count() return { "num_applicants": self.program.applicants.count(), - "num_scored": self.request.user.scores.filter( - applicant__program=self.program - ).count(), + "num_scored": self.request.user.scores.filter(applicant__program=self.program).count(), "users": users, } @@ -132,9 +128,7 @@ def get_form_class(self): "text": forms.CharField, "textarea": forms.CharField, "integer": forms.IntegerField, - }[question.type]( - required=question.required, widget=widget, label=question.question - ) + }[question.type](required=question.required, widget=widget, label=question.question) return type("ApplicationForm", (BaseApplyForm,), fields) def form_valid(self, form): @@ -182,16 +176,13 @@ def get_queryset(self): # but don't let a user see their own request applicants = list( self.program.applicants.exclude( - Q(email=self.request.user.email) | - Q(name=self.request.user.get_full_name()) + Q(email=self.request.user.email) | Q(name=self.request.user.get_full_name()) ) .prefetch_related("scores") .order_by("-applied") ) for applicant in applicants: - applicant.has_scored = applicant.scores.filter( - user=self.request.user - ).exists() + applicant.has_scored = applicant.scores.filter(user=self.request.user).exists() if applicant.has_scored: applicant.average_score = applicant.average_score() else: @@ -223,16 +214,13 @@ def get(self, request, *args, **kwargs): # but don't let a user see their own request applicants = list( self.program.applicants.exclude( - Q(email=self.request.user.email) | - Q(name=self.request.user.get_full_name()) + Q(email=self.request.user.email) | Q(name=self.request.user.get_full_name()) ) .prefetch_related("scores") .order_by("-applied") ) for applicant in applicants: - applicant.has_scored = applicant.scores.filter( - user=self.request.user - ).exists() + applicant.has_scored = applicant.scores.filter(user=self.request.user).exists() if applicant.has_scored: applicant.average_score = applicant.average_score() else: @@ -294,16 +282,13 @@ class ProgramApplicantView(ProgramMixin, TemplateView): def get(self, request, applicant_id): applicant = self.program.applicants.exclude( - Q(email=self.request.user.email) | - Q(name=self.request.user.get_full_name()) + Q(email=self.request.user.email) | Q(name=self.request.user.get_full_name()) ).get(pk=applicant_id) questions = list(self.program.questions.order_by("order")) for question in questions: question.answer = question.answers.filter(applicant=applicant).first() # See if we already scored this one - score = Score.objects.filter( - applicant=applicant, user=self.request.user - ).first() + score = Score.objects.filter(applicant=applicant, user=self.request.user).first() old_score = score.score if score else None if score: all_scores = Score.objects.filter(applicant=applicant) @@ -318,11 +303,7 @@ def get(self, request, applicant_id): new_score.user = self.request.user if old_score and new_score.score != old_score: new_score.score_history = ",".join( - [ - x.strip() - for x in (new_score.score_history or "").split(",") - if x.strip() - ] + [x.strip() for x in (new_score.score_history or "").split(",") if x.strip()] + ["%.1f" % old_score] ) new_score.save() @@ -353,9 +334,9 @@ class RandomUnscoredApplicant(ProgramMixin, View): def get(self, request): applicant = ( self.program.applicants.exclude( - Q(scores__user=self.request.user) | - Q(email=self.request.user.email) | - Q(name=self.request.user.get_full_name()) + Q(scores__user=self.request.user) + | Q(email=self.request.user.email) + | Q(name=self.request.user.get_full_name()) ) .order_by("?") .first() diff --git a/grorg/settings.py b/grorg/settings.py index ce83d12..e3e7a0e 100644 --- a/grorg/settings.py +++ b/grorg/settings.py @@ -71,6 +71,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", ) ROOT_URLCONF = "grorg.urls" @@ -117,7 +118,7 @@ # Django Allauth settings -ACCOUNT_AUTHENTICATION_METHOD = "email" +ACCOUNT_LOGIN_METHODS = {"email"} ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = "none" diff --git a/justfile b/justfile index 17b9ba9..349c0d1 100644 --- a/justfile +++ b/justfile @@ -9,13 +9,13 @@ set dotenv-load := false @fmt: just --fmt --unstable -@pre-commit: - pre-commit run --all-files +@lint *ARGS: + uv --quiet run --with pre-commit-uv pre-commit run {{ ARGS }} --all-files @up: python manage.py runserver @update: - pip install -U pip pip-tools + pip install -U pip pip-tools uv pip install -U -r requirements.in pip-compile requirements.in diff --git a/pyproject.toml b/pyproject.toml index 349fb39..bdc2eee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,57 +1,59 @@ +[project] +dependencies = [] +description = "Add your description here" +name = "grorg" +readme = "README.md" +requires-python = ">=3.9" +version = "0.1.0" + [tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "settings.test" # addopts = "--cov=. --durations=1 --nomigrations --reuse-db -vv" addopts = "--nomigrations --reuse-db -vv" -DJANGO_SETTINGS_MODULE = "settings.test" norecursedirs = ".git config node_modules scss settings static templates" python_files = "test_*.py" [tool.ruff] -# Enable Pyflakes `E` and `F` codes by default. -select = ["E", "F"] -ignore = ["E501", "E741"] # temporary - -# Allow autofix for all enabled rules (when `--fix`) is provided. -fixable = ["A", "B", "C", "D", "E", "F"] -unfixable = [] - +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # Exclude a variety of commonly ignored directories. exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".github", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "CONTRIBUTORS.md", - "dist", - "migrations", - "node_modules", - "settings/docker.py", - "static", - "venv", + ".bzr", + ".direnv", + ".eggs", + ".git", + ".github", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "CONTRIBUTORS.md", + "dist", + "migrations", + "node_modules", + "settings/docker.py", + "static", + "venv" ] - -per-file-ignores = {} - +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "B", "C", "D", "E", "F"] +ignore = ["E501", "E741"] # temporary # Same as Black. line-length = 120 - -# Allow unused variables when underscore-prefixed. -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - +per-file-ignores = {} +# Enable Pyflakes `E` and `F` codes by default. +select = ["E", "F"] # Assume Python 3.9 target-version = "py39" +unfixable = [] [tool.ruff.mccabe] # Unlike Flake8, default to a complexity level of 10. diff --git a/requirements.in b/requirements.in index 36540bf..4598f3a 100644 --- a/requirements.in +++ b/requirements.in @@ -4,5 +4,6 @@ Django<5.0 environs[django] gunicorn psycopg2-binary +requests urlman whitenoise diff --git a/requirements.txt b/requirements.txt index 7012330..473fe1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,86 +4,62 @@ # # pip-compile requirements.in # -asgiref==3.5.2 - # via django -bumpver==2023.1124 +asgiref==3.8.1 + # via + # django + # django-allauth +bumpver==2024.1130 # via -r requirements.in -certifi==2022.12.7 +certifi==2025.1.31 # via requests -cffi==1.15.1 - # via cryptography -charset-normalizer==3.1.0 +charset-normalizer==3.4.1 # via requests -click==8.1.3 +click==8.1.8 # via bumpver colorama==0.4.6 # via bumpver -cryptography==40.0.2 - # via pyjwt -defusedxml==0.7.1 - # via python3-openid -dj-database-url==0.5.0 +dj-database-url==2.3.0 # via environs -dj-email-url==1.0.5 +dj-email-url==1.0.6 # via environs -django==3.2.13 +django==4.2.19 # via # -r requirements.in + # dj-database-url # django-allauth -django-allauth==0.54.0 +django-allauth==65.4.1 # via -r requirements.in -django-cache-url==3.4.2 +django-cache-url==3.4.5 # via environs -environs[django]==9.5.0 +environs[django]==14.1.1 # via -r requirements.in -gunicorn==20.1.0 +gunicorn==23.0.0 # via -r requirements.in -idna==3.4 +idna==3.10 # via requests lexid==2021.1006 # via bumpver -looseversion==1.2.0 - # via bumpver -marshmallow==3.16.0 +marshmallow==3.26.1 # via environs -oauthlib==3.2.2 - # via requests-oauthlib -packaging==21.3 - # via marshmallow -pathlib2==2.3.7.post1 - # via bumpver -psycopg2-binary==2.9.3 +packaging==24.2 + # via + # gunicorn + # marshmallow +psycopg2-binary==2.9.10 # via -r requirements.in -pycparser==2.21 - # via cffi -pyjwt[crypto]==2.6.0 - # via django-allauth -pyparsing==3.0.9 - # via packaging -python-dotenv==0.20.0 +python-dotenv==1.0.1 # via environs -python3-openid==3.2.0 - # via django-allauth -pytz==2022.1 - # via django -requests==2.30.0 - # via - # django-allauth - # requests-oauthlib -requests-oauthlib==1.3.1 - # via django-allauth -six==1.16.0 - # via pathlib2 -sqlparse==0.4.2 +requests==2.32.3 + # via -r requirements.in +sqlparse==0.5.3 # via django toml==0.10.2 # via bumpver -urllib3==2.0.2 +typing-extensions==4.12.2 + # via dj-database-url +urllib3==2.3.0 # via requests -urlman==2.0.1 +urlman==2.0.2 # via -r requirements.in -whitenoise==6.2.0 +whitenoise==6.9.0 # via -r requirements.in - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/templates/_field.html b/templates/_field.html index bda8d54..2c0f192 100644 --- a/templates/_field.html +++ b/templates/_field.html @@ -1,8 +1,8 @@
Resource | -Amount | -- |
---|---|---|
{{ allocation.resource }} | -{{ allocation.amount }} | -- - | -
No allocations. | -
Resource | +Amount | ++ |
---|---|---|
{{ allocation.resource }} | +{{ allocation.amount }} | ++ + | +
No allocations. | +
{{ applicant.email }}
+{{ applicant.email }}
-{{ applicant.applied|date:"j M Y P" }}
+{{ applicant.applied|date:"j M Y P" }}
- {% for question in questions %} -- {{ question.answer.answer|linebreaksbr }} -
- {% else %} -- Not answered. -
- {% endif %} - {% endfor %} + {% for question in questions %} ++ {{ question.answer.answer|linebreaksbr }} +
+ {% else %} ++ Not answered. +
+ {% endif %} + {% endfor %} -- You must score this applicant before you can see other scores. -
- {% else %} -User | -Score | -Comment | -
---|---|---|
{{ ascore.user }} | -{{ ascore.score|floatformat:"1" }} | -
- {{ ascore.comment }}
- {% if ascore.score_history %}
- Score history: {{ ascore.score_history_human }} - {% endif %} - |
-
+ You must score this applicant before you can see other scores. +
+ {% else %} +User | +Score | +Comment | +
---|---|---|
{{ ascore.user }} | +{{ ascore.score|floatformat:"1" }} | +
+ {{ ascore.comment }}
+ {% if ascore.score_history %}
+ Score history: {{ ascore.score_history_human }} + {% endif %} + |
+
- This is the semi-experimental system to help manage grant applications. - Please report problems in our GitHub discussions. -
- {% if user.is_authenticated %} -No programs are available to you at this time.
- {% endfor %} - {% endif %} -Cheers to Andrew Godwin for developing grorg.
-+ This is the semi-experimental system to help manage grant applications. + Please report problems in our GitHub discussions. +
+ {% if user.is_authenticated %} +No programs are available to you at this time.
+ {% endfor %} + {% endif %} +Cheers to Andrew Godwin for developing grorg.
Name | -Applied {% if sort == "applied" %}{% endif %} | -Score {% if sort == "score" %}{% endif %} | -Allocated | -- | ||||
---|---|---|---|---|---|---|---|---|
{{ applicant.name }} | -{{ applicant.email }} | -{{ applicant.applied|date:"j M Y P" }} | - {% if applicant.has_scored %} -- {{ applicant.average_score|floatformat:"1"|default:"-" }} - ({{ applicant.scores.count }} vote{{ applicant.scores.count|pluralize }}, σ={{ applicant.stdev|floatformat:"1" }}) - | -- {% for allocation in applicant.allocations.all %} - {{ allocation.amount }} - {% empty %} - (none) - {% endfor %} - | -- View - Allocate - | - {% else %} -(hidden) | -(hidden) | -- Score - | - {% endif %} -
No applicants. | -
Name | +Applied {% if sort == "applied" %}{% endif %} | +Score {% if sort == "score" %}{% endif %} | +Allocated | ++ | ||||
---|---|---|---|---|---|---|---|---|
{{ applicant.name }} | +{{ applicant.email }} | +{{ applicant.applied|date:"j M Y P" }} | + {% if applicant.has_scored %} ++ {{ applicant.average_score|floatformat:"1"|default:"-" }} + ({{ applicant.scores.count }} vote{{ applicant.scores.count|pluralize }}, σ={{ applicant.stdev|floatformat:"1" }}) + | ++ {% for allocation in applicant.allocations.all %} + {{ allocation.amount }} + {% empty %} + (none) + {% endfor %} + | ++ View + Allocate + | + {% else %} +(hidden) | +(hidden) | ++ Score + | + {% endif %} +
No applicants. | +
Thanks for applying!
-Thanks for applying!
Loaded {{ successful }} row{{ successful|pluralize }} successfully.
- {% if errors %} -Row {{ offset }}: {{ error }}
- {% endfor %} - {% endif %} - {% endif %} -Loaded {{ successful }} row{{ successful|pluralize }} successfully.
+ {% if errors %} +Row {{ offset }}: {{ error }}
+ {% endfor %} + {% endif %} + {% endif %}Loaded {{ successful }} row{{ successful|pluralize }} successfully.
- {% if errors %} -Row {{ offset }}: {{ error }}
- {% endfor %} - {% endif %} - {% endif %} -Loaded {{ successful }} row{{ successful|pluralize }} successfully.
+ {% if errors %} +Row {{ offset }}: {{ error }}
+ {% endfor %} + {% endif %} + {% endif %}- {{ num_applicants }} application{{ num_applicants|pluralize }} received, - of which you have scored {{ num_scored }}. -
-- View applicants - {% if num_scored < num_applicants %} - Score random applicant - {% endif %} -
++ {{ num_applicants }} application{{ num_applicants|pluralize }} received, + of which you have scored {{ num_scored }}. +
++ View applicants + {% if num_scored < num_applicants %} + Score random applicant + {% endif %} +
-
- {% for resource in program.resources.all %}
- {{ resource }}:
- {{ resource.amount_allocated }} allocated, {{ resource.amount_remaining }} remaining
- {% empty %}
- No resources defined.
- {% endfor %}
-
- View resources -
+
+ {% for resource in program.resources.all %}
+ {{ resource }}:
+ {{ resource.amount_allocated }} allocated, {{ resource.amount_remaining }} remaining
+ {% empty %}
+ No resources defined.
+ {% endfor %}
+
+ View resources +
-The following users are assigned to this program:
-- Other users can sign up using the program code, which is - {{ program.join_code }}. Only give this code to people - you trust; anyone with it can sign up and see voting results. -
-The following users are assigned to this program:
++ Other users can sign up using the program code, which is + {{ program.join_code }}. Only give this code to people + you trust; anyone with it can sign up and see voting results. +
Question | -Type | -Required | -Actions | -
---|---|---|---|
Name | -Text | -- | Built in | -
- | Built in | -||
{{ question.question }} | -{{ question.get_type_display }} | -- {% if question.required %} - - {% else %} - - {% endif %} - | -- Edit - | -
Question | +Type | +Required | +Actions | +
---|---|---|---|
Name | +Text | ++ | Built in | +
+ | Built in | +||
{{ question.question }} | +{{ question.get_type_display }} | ++ {% if question.required %} + + {% else %} + + {% endif %} + | ++ Edit + | +
Name | -Type | -Amount | -Actions | -
---|---|---|---|
{{ resource.name }} | -{{ resource.get_type_display }} | -{{ resource.amount }} | -- Edit - | -
No resources set. | -
Name | +Type | +Amount | +Actions | +
---|---|---|---|
{{ resource.name }} | +{{ resource.get_type_display }} | +{{ resource.amount }} | ++ Edit + | +
No resources set. | +