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

various tweaks #29

Merged
merged 18 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ need to edit some settings.
# install requirements
$ pip install -r requirements-dev.txt

# install git hooks
$ pre-commit install
$ pre-commit run -a

# create env file and edit the settings as needed (or export settings directly)
$ cp .env_template .env

Expand All @@ -29,6 +33,7 @@ need to edit some settings.
$ inv build-js [-w]

# run Django
$ ./manage.py migrate
$ ./manage.py runserver

## Basic Commands
Expand All @@ -45,6 +50,10 @@ Some useful command are available via the `tasks.py` file:

$ python manage.py createsuperuser

- To promote a user to superuser, use this command:

$ python manage.py promote_user_to_superuser <email>

For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.

### Test coverage
Expand Down
5 changes: 1 addition & 4 deletions commcare_connect/opportunity/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class OrganizationUserMixin(LoginRequiredMixin, UserPassesTestMixin):
def test_func(self):
return self.request.user.organizations.filter(organization__slug=self.kwargs.get("org_slug")).exists()
return self.request.org_membership is not None


class OpportunityList(OrganizationUserMixin, ListView):
Expand Down Expand Up @@ -44,9 +44,6 @@ def get_form_kwargs(self):
kwargs["org_slug"] = self.kwargs.get("org_slug")
return kwargs

def test_func(self):
return self.request.user.organizations.filter(organization__slug=self.kwargs.get("org_slug")).exists()


class OpportunityEdit(OrganizationUserMixin, UpdateView):
model = Opportunity
Expand Down
20 changes: 12 additions & 8 deletions commcare_connect/templates/base.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% load static i18n %}<!DOCTYPE html>
{% load static i18n %}
{% load active_link %}<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}">
<head>
Expand Down Expand Up @@ -38,30 +39,33 @@

<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="{% url 'home' %}">Home <span class="visually-hidden">(current)</span></a>
<li class="nav-item">
<a class="nav-link {% active_link "home" %}" href="{% url 'home' %}">Home <span class="visually-hidden">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'about' %}">About</a>
<a class="nav-link {% active_link "about" %}" href="{% url 'about' %}">About</a>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'users:detail' request.user.pk %}">{% translate "My Profile" %}</a>
<a class="nav-link {% active_link "list || create || edit" namespace='opportunity' %}" href="{% url 'opportunity:list' request.org.slug %}">{% translate "Opportunities" %}</a>
</li>
<li class="nav-item">
<a class="nav-link {% active_link "users:detail" %}" href="{% url 'users:detail' request.user.pk %}">{% translate "My Profile" %}</a>
</li>
<li class="nav-item">
{# URL provided by django-allauth/account/urls.py #}
<a class="nav-link" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
<a class="nav-link {% active_link "account_logout" %}" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
</li>
{% else %}
{% if ACCOUNT_ALLOW_REGISTRATION %}
<li class="nav-item">
{# URL provided by django-allauth/account/urls.py #}
<a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% translate "Sign Up" %}</a>
<a id="sign-up-link" class="nav-link {% active_link "account_signup" %}" href="{% url 'account_signup' %}">{% translate "Sign Up" %}</a>
</li>
{% endif %}
<li class="nav-item">
{# URL provided by django-allauth/account/urls.py #}
<a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% translate "Sign In" %}</a>
<a id="log-in-link" class="nav-link {% active_link "account_login" %}" href="{% url 'account_login' %}">{% translate "Sign In" %}</a>
</li>
{% endif %}
</ul>
Expand Down
4 changes: 3 additions & 1 deletion commcare_connect/templates/opportunity/opportunity_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@
</tr>
{% empty %}
<tr>
<td>No articles yet.</td>
<td colspan="3">No opportunities yet.</td>
</tr>
{% endfor %}
</tbody>
</table>

{% if page_obj.num_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
Expand Down Expand Up @@ -80,5 +81,6 @@
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% endblock content %}
11 changes: 6 additions & 5 deletions commcare_connect/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ class UserAdmin(auth_admin.UserAdmin):
)


class UserOrganizationMembershipInline(admin.TabularInline):
list_display = ["organization", "user", "role"]
model = UserOrganizationMembership


@admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
form = OrganizationCreationForm
list_display = ["name", "created_by"]
search_fields = ["name"]
ordering = ["name"]


@admin.register(UserOrganizationMembership)
class UserOrganizationMembershipAdmin(admin.ModelAdmin):
list_display = ["organization", "user", "role"]
inlines = [UserOrganizationMembershipInline]
16 changes: 16 additions & 0 deletions commcare_connect/users/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from commcare_connect.users.models import Organization


def get_organization_for_request(request, view_kwargs):
if not request.user.is_authenticated:
return

org_slug = view_kwargs.get("org_slug", None)
if org_slug:
try:
return Organization.objects.get(slug=org_slug, memberships__user=request.user)
except Organization.DoesNotExist:
return None

membership = request.user.memberships.select_related("organization").first()
return membership.organization if membership else None
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand, CommandError

from commcare_connect.users.models import User


class Command(BaseCommand):
help = "Promotes the given user to a superuser and provides admin access."

def add_arguments(self, parser):
parser.add_argument("email", type=str)

def handle(self, email, **options):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
raise CommandError(f"No user with email {email} found!")
user.is_superuser = True
user.is_staff = True
user.save()
print(f"{email} successfully promoted to superuser and can now access the admin site")
31 changes: 31 additions & 0 deletions commcare_connect/users/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject

from .helpers import get_organization_for_request
from .models import UserOrganizationMembership as Membership


def _get_organization(request, view_kwargs):
if not hasattr(request, "_cached_org"):
team = get_organization_for_request(request, view_kwargs)
request._cached_org = team
return request._cached_org


def _get_org_membership(request):
if not hasattr(request, "_cached_org_membership"):
org = request.org
membership = None
if org:
try:
membership = Membership.objects.get(organization=org, user=request.user) if org else None
except Membership.DoesNotExist:
pass
request._cached_org_membership = membership
return request._cached_org_membership


class OrganizationMiddleware(MiddlewareMixin):
def process_view(self, request, view_func, view_args, view_kwargs):
request.org = SimpleLazyObject(lambda: _get_organization(request, view_kwargs))
request.org_membership = SimpleLazyObject(lambda: _get_org_membership(request))
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.1 on 2023-07-25 09:24

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("users", "0002_organization_userorganizationmembership"),
]

operations = [
migrations.AlterField(
model_name="userorganizationmembership",
name="organization",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="memberships", to="users.organization"
),
),
migrations.AlterField(
model_name="userorganizationmembership",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.DO_NOTHING, related_name="memberships", to=settings.AUTH_USER_MODEL
),
),
]
6 changes: 2 additions & 4 deletions commcare_connect/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,11 @@ class Role(models.TextChoices):
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="users",
related_query_name="user",
related_name="memberships",
)
user = models.ForeignKey(
User,
on_delete=models.DO_NOTHING,
related_name="organizations",
related_query_name="organization",
related_name="memberships",
)
role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER)
8 changes: 4 additions & 4 deletions commcare_connect/users/tests/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ def test_detail(user: User):


def test_update():
assert reverse("users:update") == "/users/~update/"
assert resolve("/users/~update/").view_name == "users:update"
assert reverse("users:update") == "/users/update/"
assert resolve("/users/update/").view_name == "users:update"


def test_redirect():
assert reverse("users:redirect") == "/users/~redirect/"
assert resolve("/users/~redirect/").view_name == "users:redirect"
assert reverse("users:redirect") == "/users/redirect/"
assert resolve("/users/redirect/").view_name == "users:redirect"
4 changes: 2 additions & 2 deletions commcare_connect/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

app_name = "users"
urlpatterns = [
path("~redirect/", view=user_redirect_view, name="redirect"),
path("~update/", view=user_update_view, name="update"),
path("redirect/", view=user_redirect_view, name="redirect"),
path("update/", view=user_update_view, name="update"),
path("<int:pk>/", view=user_detail_view, name="detail"),
]
3 changes: 3 additions & 0 deletions commcare_connect/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False

def get_redirect_url(self):
organization = self.request.org
if organization:
return reverse("opportunity:list", kwargs={"org_slug": organization.slug})
return reverse("users:detail", kwargs={"pk": self.request.user.pk})


Expand Down
Empty file.
6 changes: 6 additions & 0 deletions commcare_connect/web/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class WebAppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "commcare_connect.web"
Empty file.
39 changes: 39 additions & 0 deletions commcare_connect/web/templatetags/active_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django import template

register = template.Library()


@register.simple_tag(takes_context=True)
def active_link(context, viewnames, css_class="active", inactive_class="", namespace=None, *args, **kwargs):
"""
Renders the given CSS class if the request path matches the path of the view.
:param context: The context where the tag was called. Used to access the request object.
:param viewnames: The name of the view or views separated by || (include namespaces if any).
:param css_class: The CSS class to render.
:param inactive_class: The CSS class to render if the views is not active.
:param namespace: The namespace of the view or views. This can also be provided in the view name.
:return:
"""
request = context.get("request")
if request is None:
# Can't work without the request object.
return ""
current_url_name = request.resolver_match.url_name
namespaces = request.resolver_match.namespaces
active = False
views = viewnames.split("||")
for view_name in views:
view_namespace = namespace
if ":" in view_name:
view_namespace, view_name = view_name.split(":", 1)

if not view_namespace or view_namespace in namespaces:
active = current_url_name == view_name.strip()

if active:
break

if active:
return css_class

return inactive_class
2 changes: 2 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"commcare_connect.users",
"commcare_connect.opportunity",
"commcare_connect.commcarehq_provider",
"commcare_connect.web",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
Expand Down Expand Up @@ -136,6 +137,7 @@
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"commcare_connect.users.middleware.OrganizationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
Expand Down
Loading