diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f99432f7b8e23c77fe87d5814ca1c4caef6a076a..c6c9dc6962180bf35063cae6e8a9f72136391892 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,6 +45,7 @@ deploy_testing: on_stop: stop_testing except: - master + - merge_requests deploy_staging: stage: deploy diff --git a/README.md b/README.md index 162d4fcb4b03e594f10adf79af019e3009880110..4cc8eea0326b5f6bc4b8eab548d18bbb67f36349 100644 --- a/README.md +++ b/README.md @@ -49,19 +49,18 @@ Workflow Design Environment Setup <a name="envsetup"></a> ----------------- -- This project uses Python 3.7 -- Install `python-virtualenv` +- This project uses Python version >= 3.5 +- Install packages `apt-get install python3-virtualenv python3-dev build-essential libldap2-dev libsasl2-dev sqlite3` - `git clone https://gitlab.ucc.asn.au/frekk/uccportal uccportal` - `cd uccportal` - `virtualenv env` - Every time you want to do some uccportal development, do `source env/bin/activate` to set up your environment -- Install packages needed by pip to build python dependencies: `apt-get install build-essential libldap2-dev libsasl2-dev` - Install python dependencies to local environment: `pip install -r pip-packages.txt` -- Configure django: `cp gms/gms/settings_local.example.py gms/gms/settings_local.py` - - Edit `gms/gms/settings_local.py` and check that the database backend is configured correctly. (sqlite3 is fine for development) -- Initialise the database: `gms/manage.py makemigrations && gms/manage.py migrate` - - Make sure you run this again if you make any changes to `gms/memberdb/models.py` to keep the DB schema in sync. -- Run the local development server with `gms/manage.py runserver` +- Configure django: `cp src/gms/settings_local.example.py src/gms/settings_local.py` + - Edit `src/gms/settings_local.py` and check that the database backend is configured correctly. (sqlite3 is fine for development) +- Initialise the database: `src/manage.py makemigrations memberdb squarepay && src/manage.py migrate memberdb squarepay` + - Make sure you run this again if you make any changes to `src/memberdb/models.py` to keep the DB schema in sync. +- Run the local development server with `src/manage.py runserver` ----------------------------------------------------------- @@ -97,9 +96,9 @@ is configured to run as an unprivileged user (ie. `www-data`) WSGIDaemonProcess uccportal python-home=/services/uccportal/env python-path=/services/uccportal/gms WSGIProcessGroup uccportal - WSGIScriptAlias / /services/uccportal/gms/gms/wsgi.py + WSGIScriptAlias / /services/uccportal/src/gms/wsgi.py - <Directory /services/uccportal/gms/gms> + <Directory /services/uccportal/src/gms> <Files wsgi.py> Require all granted </Files> @@ -107,11 +106,11 @@ is configured to run as an unprivileged user (ie. `www-data`) Protocols h2 http:/1.1 - <Directory /services/uccportal/gms/static> + <Directory /services/uccportal/media> Require all granted </Directory> - Alias /media /services/uccportal/gms/static + Alias /media /services/uccportal/media SSLEngine On SSLCertificateFile /etc/letsencrypt/live/portal.ucc.asn.au/cert.pem @@ -124,11 +123,11 @@ is configured to run as an unprivileged user (ie. `www-data`) ``` 4. Configure django. - Follow the steps from [Environment Setup](#envsetup) - - `chmod 640 /services/uccportal/gms/gms/settings_local.py` + - `chmod 640 /services/uccportal/src/gms/settings_local.py` - `chgrp -R www-data /services/uccportal/` - `mkdir /var/log/apache2/uccportal && chgrp www-data /var/log/apache2/uccportal && chmod 775 /var/log/apache2/uccportal && chmod o+x /var/log/apache2` - Put the static files in the correct location for apache2 to find them: - - `gms/manage.py collectstatic` + - `src/manage.py collectstatic` Configuring the database backend @@ -145,22 +144,22 @@ postgres=# CREATE USER uccportal WITH ENCRYPTED PASSWORD 'insert-password-here'; postgres=# GRANT ALL on DATABASE uccportal to uccportal; ``` -Adjust `/services/uccportal/gms/gms/settings_local.py` to point to the new database (usually +Adjust `/services/uccportal/src/gms/settings_local.py` to point to the new database (usually changing the databse name is enough). Making changes to data being collected -------------------------------------- -Edit `/service/uccportal/gms/memberdb/models.py` -In `/services/uccportal/gms`, run `./manage.py makemigrations` to prepare the databae +Edit `/service/uccportal/src/memberdb/models.py` +In `/services/uccportal/src`, run `./manage.py makemigrations` to prepare the databae updates. ``` -uccportal:~# cd /services/uccportal/gms/ -uccportal:/services/uccportal/gms# ./manage.py check +uccportal:~# cd /services/uccportal/src/ +uccportal:/services/uccportal/src# ./manage.py check System check identified no issues (0 silenced). -uccportal:/services/uccportal/gms# ./manage.py migrate --run-syncdb +uccportal:/services/uccportal/src# ./manage.py migrate --run-syncdb ... You just installed Django's auth system, which means you don't have any @@ -168,7 +167,7 @@ You just installed Django's auth system, which means you don't have any Would you like to create one now? (yes/no): no Now restart MemberDB by runing -uccportal:/services/uccportal/gms# touch gms/wsgi.py +uccportal:/services/uccportal/src# touch gms/wsgi.py ``` Now go ahead and log in to the website. It will be totally fresh, with all @@ -186,4 +185,4 @@ from the Actions menu. Credits ------- - Adapted from `Gumby Management System` written by David Adam <zanchey@ucc.gu.uwa.edu.au> -- Derived from MemberDB by Danni Madeley +- Derived from MemberDB by Danni Madeley \ No newline at end of file diff --git a/pip-packages.txt b/pip-packages.txt index 1db0727343abf1e89c77d8d19d21f4812fef8214..e0807192c82ff1cb4905e1651fc1efb0a47a8301 100644 --- a/pip-packages.txt +++ b/pip-packages.txt @@ -1,15 +1,16 @@ -certifi==2018.11.29 -Django==2.1.5 +certifi==2019.3.9 +Django==2.2 django-auth-ldap==1.7.0 django-formtools==2.1 django-sslserver==0.20 -psycopg2-binary==2.7.6.1 -pyasn1==0.4.4 -pyasn1-modules==0.2.2 -python-dateutil==2.7.5 -python-ldap==3.1.0 -pytz==2018.7 +ldap3==2.6 +psycopg2-binary==2.8.2 +pyasn1==0.4.5 +pyasn1-modules==0.2.4 +python-dateutil==2.8.0 +python-ldap==3.2.0 +pytz==2019.1 six==1.12.0 -squareconnect==2.20181212.0 -urllib3==1.24.1 -ldap3==2.5.2 +sqlparse==0.3.0 +squareconnect==2.20190410.0 +urllib3==1.25 diff --git a/src/gms/settings.py b/src/gms/settings.py index c86e13f50406aa236ac4b094909882090dd9cded..9dfb8a715859e6fdffa1eeafc675faf7976fc48c 100644 --- a/src/gms/settings.py +++ b/src/gms/settings.py @@ -129,24 +129,28 @@ LOGGING = { 'django': { 'handlers':['logfile', 'console'], 'propagate': True, - 'level': LOG_LEVEL, + 'level': LOG_LEVEL_DJANGO, }, 'django.db.backends': { 'handlers': ['logfile', 'console'], - 'level': LOG_LEVEL, + 'level': LOG_LEVEL_DJANGO, 'propagate': False, }, 'django.contrib.auth': { 'handlers': ['logfile', 'console'], - 'level': LOG_LEVEL, + 'level': LOG_LEVEL_DJANGO, }, 'django_auth_ldap': { - 'level': LOG_LEVEL, + 'level': LOG_LEVEL_DJANGO, 'handlers': ['logfile', 'console'], }, 'squarepay': { 'level': LOG_LEVEL, 'handlers': ['logfile', 'console'], + }, + 'memberdb': { + 'level': LOG_LEVEL, + 'handlers': ['logfile', 'console'], } }, } diff --git a/src/gms/settings_local.example.py b/src/gms/settings_local.example.py index e4d608c7812e08d1fe996ae4181f94f481b3c47a..9be09b5d7f01fd94a4ead187e90843a65d54a26c 100644 --- a/src/gms/settings_local.example.py +++ b/src/gms/settings_local.example.py @@ -16,7 +16,7 @@ ADMINS = ( ### Database connection options ### DATABASES = { 'default': { - 'ENGINE': '${DB_ENGINE}', # Add 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'ENGINE': '${DB_ENGINE}', # django.db.backends.XXX where XXX is 'postgresql', 'mysql', 'sqlite3' or 'oracle'. # this should end up in uccportal/.db/members.db 'NAME': '${DB_NAME}', # Or path to database file if using sqlite3. 'USER': '${DB_USER}', # Not used with sqlite3. @@ -41,17 +41,12 @@ SECRET_KEY = '${APP_SECRET}' ALLOWED_HOSTS = ['${DEPLOY_HOST}'] LOG_LEVEL = 'DEBUG' +LOG_LEVEL_DJANGO = 'WARNING' LOG_FILENAME = os.path.join(ROOT_DIR, "django.log") import ldap from django_auth_ldap.config import LDAPSearch, ActiveDirectoryGroupType, LDAPGroupQuery -# LDAP admin settings -LDAP_BASE_DN = 'DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au' -LDAP_USER_SEARCH_DN = 'CN=Users,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au' -LDAP_BIND_DN = 'CN=uccportal,CN=Users,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au' -LDAP_BIND_SECRET = "${LDAP_SECRET}" - # this could be ad.ucc.gu.uwa.edu.au but that doesn't resolve externally - # useful for testing, but should be changed in production so failover works AUTH_LDAP_SERVER_URI = 'ldaps://ad.ucc.gu.uwa.edu.au' @@ -61,16 +56,32 @@ AUTH_LDAP_GLOBAL_OPTIONS = { ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER, } -# directly attempt to authenticate users to bind to LDAP -AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True +# LDAP admin settings - NOT for django_auth_ldap +LDAP_BASE_DN = "DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au" +LDAP_USER_SEARCH_DN = 'CN=Users,' + LDAP_BASE_DN + +# settings used by memberdb LDAP backend and django_auth_ldap +AUTH_LDAP_BIND_DN = "CN=uccportal,CN=Users," + LDAP_BASE_DN +AUTH_LDAP_BIND_PASSWORD = "${LDAP_SECRET}" + +# just for django_auth_ldap +AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = False AUTH_LDAP_ALWAYS_UPDATE_USER = True AUTH_LDAP_MIRROR_GROUPS = False AUTH_LDAP_GROUP_TYPE = ActiveDirectoryGroupType() -AUTH_LDAP_FIND_GROUP_PERMS = False -AUTH_LDAP_USER_DN_TEMPLATE = 'CN=%(user)s,CN=Users,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au' +# give user permissions from Django groups corresponding to names of AD groups +AUTH_LDAP_FIND_GROUP_PERMS = True + +# speed it up by not having to search for the username, we can predict the DN +AUTH_LDAP_USER_DN_TEMPLATE = 'CN=%(user)s,CN=Users,' + LDAP_BASE_DN -AUTH_LDAP_GROUP_SEARCH = LDAPSearch("OU=Groups,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au", +# this is necessary where the user DN can't be predicted, ie. if the +# user object is named by full name rather than username +#AUTH_LDAP_USER_SEARCH = LDAPSearch('CN=Users,' + LDAP_BASE_DN, +# ldap.SCOPE_SUBTREE, "(&(objectClass=user)(sAMAccountName=%(user)s))") + +AUTH_LDAP_GROUP_SEARCH = LDAPSearch("OU=Groups," + LDAP_BASE_DN, ldap.SCOPE_SUBTREE, "(objectClass=group)") # Populate the Django user from the LDAP directory. @@ -81,19 +92,24 @@ AUTH_LDAP_USER_ATTR_MAP = { "email": "email", } -ADMIN_ACCESS_QUERY = \ - LDAPGroupQuery("CN=committee,OU=Groups,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au") | \ - LDAPGroupQuery("CN=door,OU=Groups,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au") | \ - LDAPGroupQuery("CN=wheel,OU=Groups,DC=ad,DC=ucc,DC=gu,DC=uwa,DC=edu,DC=au") +DOOR_GROUP_QUERY = LDAPGroupQuery("CN=door,OU=Groups," + LDAP_BASE_DN) +COMMITTEE_GROUP_QUERY = LDAPGroupQuery("CN=committee,OU=Groups," + LDAP_BASE_DN) +WHEEL_GROUP_QUERY = LDAPGroupQuery("CN=wheel,OU=Groups," + LDAP_BASE_DN) + +ADMIN_ACCESS_QUERY = COMMITTEE_GROUP_QUERY | DOOR_GROUP_QUERY | WHEEL_GROUP_QUERY +# assign user object flags based on group memberships (independent from permissions) AUTH_LDAP_USER_FLAGS_BY_GROUP = { # staff can login to the admin site "is_staff": ADMIN_ACCESS_QUERY, # superusers have all permissions (but also need staff to login to admin site) - "is_superuser": ADMIN_ACCESS_QUERY, + "is_superuser": COMMITTEE_GROUP_QUERY | WHEEL_GROUP_QUERY, } +# cache group memberships for 5 minutes +AUTH_LDAP_CACHE_TIMEOUT = 300 + # the Square app and location data (set to sandbox unless you want it to charge people) SQUARE_APP_ID = '${SQUARE_APP_ID}' SQUARE_LOCATION = '${SQUARE_LOCATION}' diff --git a/src/memberdb/account_backend.py b/src/memberdb/account_backend.py index a2174438c451734b5803145fe23c8cb680a7c997..bcfffeddfc6f2de6353e28698025e2e7edf6855a 100644 --- a/src/memberdb/account_backend.py +++ b/src/memberdb/account_backend.py @@ -29,8 +29,8 @@ log = logging.getLogger('ldap') ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI') ldap_user_dn = getattr(settings, 'LDAP_USER_SEARCH_DN') ldap_base_dn = getattr(settings, 'LDAP_BASE_DN') -ldap_bind_dn = getattr(settings, 'LDAP_BIND_DN') -ldap_bind_secret = getattr(settings, 'LDAP_BIND_SECRET') +ldap_bind_dn = getattr(settings, 'AUTH_LDAP_BIND_DN') +ldap_bind_secret = getattr(settings, 'AUTH_LDAP_BIND_PASSWORD') make_home_cmd = ["sudo", "/services/uccportal/src/memberdb/root_actions.py"] make_mail_cmd = 'ssh -i %s root@mooneye "/usr/local/mailman/bin/add_members" -r- ucc-announce <<< %s@ucc.asn.au' make_mail_key = './mooneye.key' diff --git a/src/memberdb/models.py b/src/memberdb/models.py index b8cb469caed55544aa75c28e40c3fd0aad0aeffc..eb6b28096dab64875bb54a38daeeecc0eba57acd 100644 --- a/src/memberdb/models.py +++ b/src/memberdb/models.py @@ -11,52 +11,53 @@ import subprocess import ldap """ -dictionary of membership types & descriptions, should be updated if these are changed in dispense. +list of membership types & descriptions, should be updated if these are changed in dispense. +note: this is a list of tuples of key and value as dict, if it was a dict the database models get confused """ -MEMBERSHIP_TYPES = { - 'oday': { +MEMBERSHIP_TYPES = [ + ('oday', { 'dispense':'pseudo:11', 'desc':'O\' Day Special - first time members only', 'is_guild':True, 'is_student':True, 'must_be_fresh':True, - }, - 'student_and_guild': { + }), + ('student_and_guild', { 'dispense':'pseudo:10', 'desc':'Student and UWA Guild member', 'is_guild':True, 'is_student':True, 'must_be_fresh':False, - }, - 'student_only': { + }), + ('student_only', { 'dispense':'pseudo:9', 'desc':'Student and not UWA Guild member', 'is_guild':False, 'is_student':True, 'must_be_fresh':False, - }, - 'guild_only': { + }), + ('guild_only', { 'dispense':'pseudo:8', 'desc':'Non-Student and UWA Guild member', 'is_guild':True, 'is_student':False, 'must_be_fresh':False, - }, - 'non_student': { + }), + ('non_student', { 'dispense':'pseudo:7', 'desc':'Non-Student and not UWA Guild member', 'is_guild':False, 'is_student':False, 'must_be_fresh':False, - }, - 'lifer': { + }), + ('lifer', { 'dispense':'', 'desc':'Life member', 'is_guild':False, 'is_student':False, 'must_be_fresh':False, - } -} + }) +] def get_membership_choices(is_renew=None, get_prices=True): """ @@ -64,7 +65,7 @@ def get_membership_choices(is_renew=None, get_prices=True): also dynamically fetch the prices from dispense (if possible) """ choices = [] - for key, val in MEMBERSHIP_TYPES.items(): + for key, val in MEMBERSHIP_TYPES: if (val['must_be_fresh'] and is_renew == True): # if you have an account already, you don't qualify for the fresher special continue @@ -90,7 +91,7 @@ def get_membership_choices(is_renew=None, get_prices=True): def get_membership_type(member): best = 'non_student' is_fresh = member.memberships.all().count() == 0 - for i, t in MEMBERSHIP_TYPES.items(): + for i, t in MEMBERSHIP_TYPES: if (t['must_be_fresh'] == is_fresh and t['is_student'] == member.is_student and t['is_guild'] == member.is_guild): best = i break @@ -225,11 +226,16 @@ class Membership (models.Model): def __str__ (self): return "Member [%s] (%s) renewed membership on %s" % (self.member.username, self.member.display_name, self.date_submitted.strftime("%Y-%m-%d")) + def get_membership_type(self): + for key, val in MEMBERSHIP_TYPES: + if key == self.membership_type: + return val + def get_dispense_item(self): - return MEMBERSHIP_TYPES[self.membership_type]['dispense'] + return self.get_membership_type()['dispense'] def get_pretty_type(self): - return MEMBERSHIP_TYPES[self.membership_type]['desc'] + return self.get_membership_type()['desc'] class Meta: verbose_name = "Membership renewal record" diff --git a/src/memberdb/register.py b/src/memberdb/register.py index 0bceb6aee7a3e57051656bc9b46c6bf19368d5e7..99ba5d15d136a699eaa5b079593c0d0bf5b3cf06 100644 --- a/src/memberdb/register.py +++ b/src/memberdb/register.py @@ -19,7 +19,7 @@ from django.conf import settings from squarepay.models import MembershipPayment from squarepay.dispense import get_item_price -from .models import Member, Membership, get_membership_choices, make_pending_membership, MEMBERSHIP_TYPES +from .models import Member, Membership, get_membership_choices, make_pending_membership from .forms import MyModelForm from .views import MyUpdateView diff --git a/src/memberdb/views.py b/src/memberdb/views.py index 334daf71bb53d08422bed4c0ebb09e381370e8b8..19e40304f5648e01fa55512b1409ca8d0db3ebcb 100644 --- a/src/memberdb/views.py +++ b/src/memberdb/views.py @@ -11,7 +11,7 @@ from django.contrib.auth.mixins import AccessMixin from django.utils import timezone from formtools.wizard.views import SessionWizardView -from .models import Member, IncAssocMember, Membership, MEMBERSHIP_TYPES, TokenConfirmation +from .models import Member, IncAssocMember, Membership, TokenConfirmation from .forms import MemberHomeForm class MemberMiddleware: @@ -39,6 +39,12 @@ class MemberMiddleware: request.member.token = None request.member.save() + if request.user.ldap_user is not None: + # copy the LDAP groups so templates can access them + request.member.groups = list(request.user.ldap_user.group_names) + else: + request.member.groups = [ "gumby" ] + # request.session is a dictionary-like object, its content is saved in the database # and only a session ID is stored as a browser cookie (by default, but is configurable) if 'member_id' in request.session: diff --git a/src/squarepay/cokelog.py b/src/squarepay/cokelog.py index 96e26eff83632dfa28ae3ae349368ee346ac48b7..0921dae57df52ef57bb019cf58a4d5869e558e06 100644 --- a/src/squarepay/cokelog.py +++ b/src/squarepay/cokelog.py @@ -10,7 +10,7 @@ if COKELOG is None: log.warning("COKELOG_PATH is not defined, cannot sync payment status from dispense to DB") ALL_REGEX = r"^(?P<date>[A-Za-z]{3}\s+\d+\s[\d:]{8})\s(\w+)\sodispense2:\sdispense '([^']+)' \((?P<item>(coke|pseudo|snack|door):(\d{1,3}))\) for (?P<for>\w+) by (?P<by>\w+) \[cost\s+(\d+), balance\s+(\d+)\]$" -MEMBERSHIP_REGEX = r"^(?P<date>[A-Za-z]{3}\s+\d+\s[\d:]{8})\s(\w+)\sodispense2:\sdispense '([^']+)' \((?P<item>(pseudo):(\d{1,3}))\) for (?P<for>\w+) by (?P<by>\w+) \[cost\s+(\d+), balance\s+(\d+)\]$" +MEMBERSHIP_REGEX = r"^(?P<date>[A-Za-z]{3}\s+\d+\s[\d:]{8})\s(\w+)\sodispense2:\sdispense '(membership [^']+)' \((?P<item>(pseudo):(\d{1,3}))\) for (?P<for>\w+) by (?P<by>\w+) \[cost\s+(\d+), balance\s+(\d+)\]$" class CokeLog: regex = ALL_REGEX @@ -83,7 +83,7 @@ class CokeLog: continue if dispense_by is not None and r["by"] != dispense_by: continue - return r["date"] + return r return None # create a "static" instance of cokelog @@ -105,15 +105,21 @@ def try_update_from_dispense(membership): else: member_cokelog.open() - # look for entries like "dispense 'membership ...' (pseudo:) for <user> by <user> ..." - ms_disp = member_cokelog.get_last_dispense( - membership.member.username, - membership.get_dispense_item(), - ) + # look for entries like "dispense 'membership ...' (pseudo:..) for <user> by <user> ..." + ms_disp = member_cokelog.get_last_dispense(membership.member.username) if ms_disp is not None: - membership.date_paid = ms_disp - membership.payment_method = 'dispense' - return True + if ms_disp['item'] != membership.get_dispense_item(): + log.warn("user '%s': paid incorrect item '%s', not '%s' in dispense." % ( + membership.member.username, ms_disp['item'], membership.get_dispense_item() + )) + else: + membership.date_paid = ms_disp['date'] + membership.payment_method = 'dispense' + log.debug("user '%s': paid in cokelog" % membership.member.username) + return True + else: + log.info("user '%s': no paid membership in cokelog" % membership.member.username) + return False - \ No newline at end of file + diff --git a/src/squarepay/migrations/0001_initial.py b/src/squarepay/migrations/0001_initial.py deleted file mode 100644 index 2bcf8bd51021f2f26872c739baef515a90eaa06c..0000000000000000000000000000000000000000 --- a/src/squarepay/migrations/0001_initial.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-29 04:02 - -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('memberdb', '__first__'), - ] - - operations = [ - migrations.CreateModel( - name='CardPayment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(max_length=255, verbose_name='Description')), - ('amount', models.IntegerField(verbose_name='Amount in cents')), - ('idempotency_key', models.CharField(default=uuid.uuid1, max_length=64, verbose_name='Square Transactions API idempotency key')), - ('is_paid', models.BooleanField(blank=True, default=False, verbose_name='Has been paid')), - ('dispense_synced', models.BooleanField(blank=True, default=False, verbose_name='Payment logged in dispense')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_paid', models.DateTimeField(blank=True, null=True, verbose_name='Date paid (payment captured)')), - ], - ), - migrations.CreateModel( - name='MembershipPayment', - fields=[ - ('cardpayment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='squarepay.CardPayment')), - ('membership', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='memberdb.Membership')), - ], - bases=('squarepay.cardpayment',), - ), - ] diff --git a/src/squarepay/migrations/__init__.py b/src/squarepay/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/squarepay/views.py b/src/squarepay/views.py index 2e62b3cb500978d796c1777e2ced2ab1d72f26c7..c80825f531d6848bf3eff53cb27033cfbb58d9e6 100644 --- a/src/squarepay/views.py +++ b/src/squarepay/views.py @@ -9,7 +9,7 @@ from django.urls import reverse from django.utils import timezone from memberdb.views import MemberAccessMixin -from memberdb.models import Membership, MEMBERSHIP_TYPES +from memberdb.models import Membership from .models import MembershipPayment, CardPayment from . import payments diff --git a/src/static/admin_custom.css b/src/static/admin_custom.css index 00ce3db898ca8871eb330fc763d319be52f92bc7..bd13a8a1b80c494dea139c81b9ae7c459e47f488 100644 --- a/src/static/admin_custom.css +++ b/src/static/admin_custom.css @@ -24,17 +24,17 @@ #header { padding: 0; height: auto; + display: block; } #admin-header { - padding: 0px 40px; + padding: 10px 40px; } #admin-header, #container { overflow: auto; } - .button { display: inline-block; } diff --git a/src/static/memberdb.css b/src/static/memberdb.css index 004be23a6dae2550e8c7fd41ee4fd8d666860070..a5059159e31511c445593d3d2687baa63919e287 100644 --- a/src/static/memberdb.css +++ b/src/static/memberdb.css @@ -105,7 +105,7 @@ nav { overflow: auto; /* make navbar expand in case the tab buttons flow to a second row */ } -.navtab { +.navtab, .userinfo { float: left; text-decoration: none; color: white; @@ -131,6 +131,47 @@ nav { border-right: 1.5px solid #555; } +.userinfo { + float: right; + border-right: 0; + margin-right: 0; + font-variant-caps: normal; + border: none; + background-color: none; + padding: 19px 20px; +} + +.userinfo.groups { + padding: 7px 20px; +} + +.userinfo .username { + display: block; + line-height: 22px; + font-size: 16px; + font-weight: 600; +} + +/* height of group tags comes to 22px */ +.userinfo .group { + display: inline-block; + border-radius: 11px; + font-size: 12px; + line-height: 14px; + padding: 5px 8px 3px 8px; + background-color: #f6b9d8; + color: #000; + margin: 0 4px; +} + +.userinfo .group.door { + background-color: #b9c6f6; +} + +.userinfo .group.wheel { + background-color: #b9f6c8; +} + .member-details, .membership-details { width: 100%; text-align: left; diff --git a/src/templates/base.html b/src/templates/base.html index 06849d276ae778da669496f2e8a8167ad4a7ae83..1710d6a2daf95b22e08de6dc1024c4853b882a7f 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -50,6 +50,17 @@ <a class="navtab {% if url_name == 'logout' %}active{% endif %}" href="{% url "memberdb:logout" %}">Logout</a> {% endif %} {% endwith %} + + {# add the groups class only if we have groups to display #} + <div class="userinfo{% if request.user.is_authenticated %}{% if request.member %} groups{% endif %}"> + <span class="username">Welcome, {{ request.user.username }}</span> + {% if request.member %} + {% for g in request.member.groups %}<span class="group {{ g }}">{{ g }}</span>{% endfor %} + {% endif %} + {% else %}"> + <span class="username">Not logged in</span> + {% endif %} + </div> </nav> {% endblock %} diff --git a/src/templates/home.html b/src/templates/home.html index e3f690a69a9be572bcd98190d461ef707bd448c9..63e2e520426e1116a19b0bbe08ec0ff36c2a8f58 100644 --- a/src/templates/home.html +++ b/src/templates/home.html @@ -15,6 +15,8 @@ {% if request.user.is_authenticated %} <b>You have no member record associated with this account.</b><br> Please <a href="{% url 'memberdb:renew' %}">renew your membership</a> to get started. + <br><br> + Note: you may have already paid for this year, but unless the username you entered is the same as the one you are using now, it will not work. {% else %} <b>Something went wrong and your membership details could not be retrieved.</b> Please try <a href="{% url 'memberdb:login' %}">logging in</a>. {% endif %}