Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop commits into changes branch #233

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3056aee
update README.md to reflect onboarding.md.
JasonLovesDoggo Nov 18, 2023
ebf915a
updated jekyll-gh-pages.yml to only run when /docs were changed on ma…
JasonLovesDoggo Nov 18, 2023
3f417ec
attempt to get jekyll-gh-pages.yml to actually deploy
JasonLovesDoggo Nov 18, 2023
2a9afe9
semi rollback jekyll-gh-pages.yml to try to get a fix.
JasonLovesDoggo Nov 18, 2023
27f584e
please work.
JasonLovesDoggo Nov 18, 2023
f691fdb
literally use the example workflow to see if at least it publishes th…
JasonLovesDoggo Nov 18, 2023
7467e9c
check if issue was ./docs
JasonLovesDoggo Nov 18, 2023
919f5db
remove qodana.yaml
JasonLovesDoggo Nov 18, 2023
92ae65b
update _config.yml
JasonLovesDoggo Nov 18, 2023
5b6e9ea
updated some reqs.
JasonLovesDoggo Nov 18, 2023
e7bd41c
updated current theme.
JasonLovesDoggo Nov 18, 2023
74cbb58
updated local_settings_sample.py
JasonLovesDoggo Nov 18, 2023
0ffec39
updated onboarding.md
JasonLovesDoggo Nov 18, 2023
833bced
fixed the broken url
JasonLovesDoggo Nov 18, 2023
eaa7d80
fix (tz): local_settings_sample.py
JasonLovesDoggo Nov 18, 2023
be529f5
updated .gitignore with some more files.
JasonLovesDoggo Nov 18, 2023
2c4cc0f
docs (onboarding): updated the docs for windows setup with some error…
JasonLovesDoggo Nov 22, 2023
c80da41
docs (onboarding): updated comment for bash
JasonLovesDoggo Nov 22, 2023
6d32da2
fix (banners): have them properly expire.
JasonLovesDoggo Nov 22, 2023
39f2179
removed debug print/pep8ify
JasonLovesDoggo Nov 22, 2023
d947f66
fix: uncommented now
JasonLovesDoggo Nov 22, 2023
f54cb59
fix: forgot to call calculate banners (bless tests)
JasonLovesDoggo Nov 22, 2023
e4ade5a
broke prod
JasonLovesDoggo Nov 22, 2023
88ffc56
broke prod v2
JasonLovesDoggo Nov 22, 2023
ee4ec99
broke prod v2
JasonLovesDoggo Nov 22, 2023
490be2f
updated comments/types for Ba
JasonLovesDoggo Nov 23, 2023
df5cb5d
fix/feat (api/v3): made CharField and TextField case insensitive.
JasonLovesDoggo Nov 23, 2023
3feb1f7
chore: removing debug prints
JasonLovesDoggo Nov 23, 2023
01d019c
chore: slight pep change
JasonLovesDoggo Nov 23, 2023
3c3ca92
refactor: moved id field from lookup_field to new method.
JasonLovesDoggo Nov 24, 2023
0358e07
style: little format change
JasonLovesDoggo Nov 24, 2023
ea8067c
refactor: simply moved var usage for better readability
JasonLovesDoggo Nov 24, 2023
5b4fdac
fixed a lil comment
JasonLovesDoggo Nov 24, 2023
cf27dad
refactor: moved api configs to settings.py
JasonLovesDoggo Nov 24, 2023
ac12857
chore: fixed types
JasonLovesDoggo Nov 24, 2023
fa5805f
fix: forgot to specify settings
JasonLovesDoggo Nov 24, 2023
59810cf
docs (onboarding): clarified one step.
JasonLovesDoggo Nov 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions .github/workflows/jekyll-gh-pages.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,51 @@

name: Jekyll
name: Docs Deployment

on:
push:
branches: ["main"]
branches: ["main", "develop"]
paths:
- 'docs/**'
- '.github/workflows/jekyll-gh-pages.yml'
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: true
cancel-in-progress: false

jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/configure-pages@v2
- uses: actions/jekyll-build-pages@v1
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./docs
source: ./
destination: ./_site
- uses: actions/upload-pages-artifact@v1
- name: Upload artifact
uses: actions/upload-pages-artifact@v2

# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- id: deployment
uses: actions/deploy-pages@v1
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ node_modules/
celerybeat-schedule
/mobile
/doodle
# editors
/.idea
.vscode/*
# django stuff
local_settings.py
*.log
26 changes: 3 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
# Metropolis (Backend)
Make sure you have python 3.9+ installed. As of now, the project is only compatible with versions between python 3.9 and 3.10.11.
## Running Locally
# Metropolis
[__Mac Lyons’ Den__](https://docs.google.com/document/d/1ycoI7KbStukZ_B89UxVFGqYHBrw5D2clpVDAL3R23GM/edit)'s backend systems.

Recommended: install [Nix](https://nixos.org/download) and [direnv](https://direnv.net) and run:
```sh
make
direnv allow
./manage.py migrate
nix run
```
If not using direnv:
```sh
nix develop # and run `./manage.py migrate` inside
nix run
```

If you do not want to use Nix:
(Note: only tested on Unix-like platforms)

```
make
poetry run python ./manage.py migrate
poetry run python ./manage.py runserver
```
### To set up metropolis locally, please follow the [setup guide](https://docs.maclyonsden.com/docs/onboarding.html)
2 changes: 1 addition & 1 deletion _config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
remote_theme: wlmac/just-the-docs

title: Docs
title: Metropolis Docs
author: Metropolis
1 change: 0 additions & 1 deletion core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from django.utils.translation import gettext_lazy as _
from martor.widgets import AdminMartorWidget

from metropolis import settings
from . import models
from .forms import (
AnnouncementAdminForm,
Expand Down
10 changes: 7 additions & 3 deletions core/api/views/meta.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import Dict

from django.conf import settings
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.views import APIView

Expand Down Expand Up @@ -29,9 +29,13 @@ def censor(cls, banner: Dict) -> Dict:

@staticmethod
def get(request):
now = timezone.now()
return Response(Banners.calculate_banners())

@classmethod
def calculate_banners(cls):
now = datetime.now(settings.TZ)
current = filter(lambda b: b["start"] < now < b["end"], settings.BANNER3)
current = list(map(Banners.censor, current))
upcoming = filter(lambda b: now <= b["start"], settings.BANNER3)
upcoming = list(map(Banners.censor, upcoming))
return Response(dict(current=current, upcoming=upcoming))
return dict(current=current, upcoming=upcoming)
9 changes: 5 additions & 4 deletions core/api/views/objects/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import List
from typing import List, Literal


class BaseProvider:
allow_list: bool = True
allow_new: bool = True
kind: str = ""
allow_list: bool = True # Is the view able to list the model's objects. (e.g. /user would list all users
allow_new: bool = True # Is the provider able to create a new object.
kind: Literal["list", "new", "single", "retrieve"] # type of view
listing_filters_ignore: List[str] = []
# ^ don't treat the passed in params as a listing filter but instead as a param for the view.

def __init__(self, request):
self.request = request
2 changes: 1 addition & 1 deletion core/api/views/objects/blog_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Meta:
class BlogPostProvider(BaseProvider):
serializer_class = Serializer
model = BlogPost
lookup_fields = ["id", "slug"]
additional_lookup_fields = ["slug"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion core/api/views/objects/exhibit.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Meta:

class ExhibitProvider(BaseProvider):
model = Exhibit
lookup_fields = ["id", "slug"]
additional_lookup_fields = ["slug"]
serializer_class = Serializer

def __init__(self, *args, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion core/api/views/objects/flatpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FlatPageProvider(BaseProvider):
serializer_class = Serializer
model = FlatPage
allow_list = True
lookup_fields = ["id", "url"]
additional_lookup_fields = ["url"]

@property
def permission_classes(self):
Expand Down
95 changes: 64 additions & 31 deletions core/api/views/objects/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import os
from json import JSONDecodeError
from typing import Dict, Callable, List, Tuple
from typing import Dict, Callable, List, Tuple, Set, Final

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, BadRequest
from django.db.models import Model, Q, QuerySet
from django.http import QueryDict
Expand Down Expand Up @@ -80,42 +83,76 @@ def initial(self, *args, **kwargs):
self.permission_classes = [permissions.AllowAny]
else:
self.permission_classes = provider.permission_classes
self.as_su = as_su
self.as_su = as_su # if the user is a SU
self.serializer_class = provider.serializer_class
self.lookup_fields = getattr(
provider, "lookup_fields", getattr(provider, "lookup_field", ["id", "pk"])
)
self.additional_lookup_fields = self._compile_lookup_fields()
self.listing_filters = getattr(
provider,
"listing_filters",
getattr(provider, "listing_filter", {"id": int, "pk": int}),
) # NOTE: better to have the following if after initial, but this is easier

def _compile_lookup_fields(self) -> Set[str]:
"""
Compiles the additional lookup fields + the two required into one clean set.
"""
allowed_fields: List = getattr( # the lookup fields allowed for the provider
self.provider, "additional_lookup_fields", []
)
# NOTE: better to have the following if after initial, but this is easier
allowed_fields.extend(settings.GLOBAL_LOOKUPS)
return set(
allowed_fields
) # use a set for better memory performance & no duplicates.

def get_object(self):
queryset = self.get_queryset()
@property
def lookup_field(self) -> str:
lookup = (
self.request.query_params.get("lookup", "id")
if self.request.query_params.get("lookup") in self.lookup_fields
if self.request.query_params.get("lookup") in self.additional_lookup_fields
else "id"
)

field_type = self.provider.model._meta.get_field(
lookup
).get_internal_type() # get value like CharField
if field_type in settings.LOOKUP_FIELD_REPLACEMENTS.keys():
lookup += settings.LOOKUP_FIELD_REPLACEMENTS[field_type]
return lookup

def validate_lookup(self) -> None:
"""
checks the lookup field to see if it's a valid lookup field for the provider.
:return: NoneType
"""
lookup = self.request.query_params.get("lookup")
if lookup is None:
return
lookup = lookup or settings.GLOBAL_LOOKUPS[0]
if lookup not in self.additional_lookup_fields:
raise BadRequest(
f"Invalid lookup field {lookup}. Valid fields are: {', '.join(self.additional_lookup_fields)}."
)

def get_object(self) -> Model | None: # None if a 404 (obj not found)
self.validate_lookup()
queryset = self.get_queryset()

q = Q()
raw = {lookup: [self.kwargs.get("lookup")]}
if lookup == "id":
if not raw[lookup][0].isdigit():
raw = {self.lookup_field: self.kwargs.get("lookup")}
if self.lookup_field == "id":
if not raw[self.lookup_field][0].isdigit():
raise BadRequest(
"ID must be an integer, if you want to use a different lookup, refer to the docs for the supported lookups."
)
filtered = False
for field in self.lookup_fields:
if field in raw:
if field in ("id", "pk") and raw[field][0] == "0":
# ignore 0 pk
continue
q |= Q(**{f"{field}__in": raw[field]})
filtered = True
if not filtered:
raise BadRequest("not enough filters")
if self.lookup_field in raw:
if (
self.lookup_field in ("id", "pk") and raw[self.lookup_field][0] == "0"
): # todo, check if needed
# ignore 0 pk
pass
q |= Q(**{self.lookup_field: raw[self.lookup_field]})
else:
raise BadRequest("Invalid filtering - Most likely a server error")

obj = get_object_or_404(queryset, q)
self.check_object_permissions(self.request, obj)
Expand Down Expand Up @@ -169,10 +206,8 @@ def dispatch(self, request, *args, **kwargs):
self.initial(request, *args, **kwargs)

# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(
self, request.method.lower(), self.http_method_not_allowed
)
if (request_type := request.method.lower()) in self.http_method_names:
handler = getattr(self, request_type, self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed

Expand Down Expand Up @@ -225,8 +260,7 @@ def __convert_query_params__(self, query_params: QueryDict) -> List[Tuple]:
for key, value in query_params.lists():
if (
key
in ["limit", "offset", "search_type", "format"]
+ self.provider.listing_filters_ignore
in settings.IGNORED_QUERY_PARAMS + self.provider.listing_filters_ignore
):
continue
if key not in self.listing_filters:
Expand Down Expand Up @@ -277,9 +311,8 @@ def __compile_filters__(query_params: List) -> Dict:

if isinstance(lookup_value, list) and len(lookup_value) > 1:
if isinstance(lookup_value[0], dict):
filters[
f"{lookup_filter}__{lookup_value['category']}__in"
] = ( # todo add option to use ID or spec field (fix option)
filters[f"{lookup_filter}__{lookup_value['category']}__in"] = (
# todo add option to use ID or specified field (fix option)
lookup_value["item"]
if not isinstance(lookup_value["item"], list)
else lookup_value["item"][0]
Expand Down
2 changes: 1 addition & 1 deletion core/api/views/objects/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class OrganizationProvider(BaseProvider):
"is_active": bool,
"is_open": bool,
}
lookup_fields = ["id", "slug"]
additional_lookup_fields = ["slug"]

@property
def permission_classes(self):
Expand Down
2 changes: 1 addition & 1 deletion core/api/views/objects/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def has_object_permission(self, request, view, user):

class UserProvider(BaseProvider):
model = User
lookup_fields = ["id", "username"]
additional_lookup_fields = ["username"]

@property
def permission_classes(self):
Expand Down
1 change: 1 addition & 0 deletions core/models/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.utils import timezone

from .choices import announcement_status_choices

# from ..api.utils.profanity import predict
from ..utils.file_upload import file_upload_path_generator

Expand Down
6 changes: 6 additions & 0 deletions core/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.db.models.functions import Lower
from django.utils import timezone

from .choices import graduating_year_choices, timezone_choices
Expand Down Expand Up @@ -101,3 +102,8 @@ def can_approve(self, obj):
@classmethod
def all(cls):
return cls.objects.filter(is_active=True)

class Meta:
constraints = [
models.UniqueConstraint(Lower("username"), name="username-lower-check")
]
5 changes: 3 additions & 2 deletions core/templates/core/base.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load common_tags %}
{% load settings_tags %}
{% load banners %}
{% load static %}

{% block title %}{% block head_title %}{{ title }}{% endblock %} | Metropolis{% endblock %}
Expand All @@ -18,8 +19,8 @@

{% settings_value "PRE" as pre %}
{% if pre %}{{ pre | safe }}{% endif %}
{% settings_value "BANNER3" as banner3 %}
{% for value in banner3 %}
{% banners "current" as current_banners %}
{% for value in current_banners %}
<div class="install-popup">
{% if value.icon_url %}
<img src="{{ value.icon_url }}" alt="Banner Icon">
Expand Down
Loading
Loading