GitLab will be upgraded on 30 Jan 2023 from 2.00 pm (AEDT) to 3.00 pm (AEDT). During the update, GitLab and Mattermost services will not be available. If you have any concerns with this, please talk to us at N110 (b) CSIT building.

Commit 11c54b21 authored by Lincoln Smith's avatar Lincoln Smith
Browse files

Initial commit.

v0.1.0 - Integrates auth system, some models change, audit and logging
into prototype.
parents
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# Stuff from testing we don't need
perfieldperms/
perfieldperms
Lincoln Smith <lincoln.smith@anu.edu.au>
Byron Vickers
Implements the Course Information Management System from CIMS/CIMS-Prototype.
Created as a Django app so pretty much drop it into your django installation. With any luck there will a setup.py in here somwhere so you can also install it using pip. Run the migrations and you're away.
## Requirements ##
* Python 3.4+
* Django 1.10+
## Required Libraries ##
* Beautiful Soup 4 (beautifulsoup4, bs4)
* html5lib
* django-auth-ldap
* django-mptt
* django-tables2
* pdfkit with wkhtmltopdf binary from http://wkhtmltopdf.org/
* pyldap
## Additional libraries ##
* doc2txt - data import
#from functools import update_wrapper
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User as UserModel
from django.forms import Textarea
from django.db.models import TextField
#from django.http import HttpResponseRedirect
from django.urls import reverse, resolve
#from django.conf.urls import url
from django.shortcuts import get_object_or_404
#from django.template.response import TemplateResponse
#from django.utils.encoding import force_text
from mptt.admin import MPTTModelAdmin
from reversion.admin import VersionAdmin
from cims import models
#from cims.settings import SITE_TITLE, SITE_HEADER
#
#admin.site.site_title = SITE_TITLE
#admin.site.site_header = SITE_HEADER
#def _filter_inline_lo_formfield(
# db_field,
# request,
# check_url,
# model,
# check_name='learningoutcomes'
# ):
# """
# Return a filtered set of learning outcomes for an inline object formfield.
#
# For objects edited inline that have a relationship to learning outcomes, we
# want to restrict the available options to those belonging to the respective
# CourseVersion instance.
#
# Positional arguments:
# db_field -- the model field instance being processed
# request -- current django request
# check_url -- string to check against django resolve object url attribute to
# make sure we're editing the right object
# model - the model whose ModelAdmin add/change page we're on
# check_name -- the field name w're interested in
# """
# if db_field.name == check_name:
# resolver = resolve(request.path_info)
# # We only want to return filtered results if we're editing a model
# # instance
# if resolver.url_name == check_url:
# instance = get_object_or_404(model, pk=resolver.args[0])
# # Work out which model we're dealing with
# if type(model()) == type(models.CourseVersion()):
# queryset = instance.learningoutcome_set.all()
# elif type(model()) == type(models.CourseInstance()):
# queryset = instance.courseversion.learningoutcome_set.all()
# else:
# # Got something we didn't expect so return empty queryset
# queryset = models.LearningOutcome.objects.none()
# else:
# # It's a new model instance so return an empty set because we
# # can't filter on standards linked from the course version level
# # yet
# queryset = models.LearningOutcome.objects.none()
# return queryset
# else:
# return None
#class CopyableModelAdmin(admin.ModelAdmin):
# """ModelAdmin with view extension for copy/duplicate functionality."""
# clear_fields = []
#
# def __init__(self, model, admin_site):
# super().__init__(model, admin_site)
#
# def get_clear_fields(self):
# if self.clear_fields:
# return self.clear_fields
# # Do stuff to get fields to clear if not specified
# return self.clear_fields
#
# def copy_view(self, request):
# """
# Render a prepopulated form to enable copying of another object.
#
# Replicates existing changeform_view() as much as possible. Uses GET
# attribute "source" for source object id.
# """
# info = self.model._meta.app_label, self.model._meta.model_name
# model = self.model
# opts = model._meta
# add = True
# obj = None
#
# if not self.has_add_permission(request):
# raise PermissionDenied
#
# if request.method == 'GET':
# source_id = request.GET.get('source')
# source_object = get_object_or_404(model, id=source_id)
# ModelForm= self.get_form(request, obj)
# prefill = dict()
# # Only copy concrete fields from instance, excluding relations
# # unless they are required i.e. field.blank=False
# for field in source_object._meta.get_fields():
# if field.concrete and (field.blank != field.is_relation):
# prefill[field.name] = getattr(source_object, field.name)
# # Remove fields we want cleared from prefill, mostly relationship
# # and required fields
# for key in clear_fields:
# del prefill[key]
#
# form = ModelForm(initial=prefill)
# formsets, inline_instances = self._create_formsets(request,
# form.instance, change=False)
# adminForm = admin.helpers.AdminForm(
# form,
# list(self.get_fieldsets(request, obj)),
# self.get_prepopulated_fields(request, obj),
# self.get_readonly_fields(request, obj),
# model_admin=self)
# media = self.media + adminForm.media
#
# inline_formsets = self.get_inline_formsets(request, formsets, inline_instances, obj)
# for inline_formset in inline_formsets:
# media = media + inline_formset.media
#
# # Just prefilling so process form at standard "add" url
# form_url = reverse('admin:{0[0]}_{0[1]}_add'.format(info))
# context = dict(
# self.admin_site.each_context(request),
# title='Add {}'.format(force_text(opts.verbose_name)),
# adminform=adminForm,
# object_id=None,
# original=obj,
# is_popup=False,
# media=media,
# inline_admin_formsets=inline_formsets,
# errors=admin.helpers.AdminErrorList(form, formsets),
# preserved_filters=self.get_preserved_filters(request),
# show_save=True,
# show_save_and_continue=True,
# form=form,
# )
# return self.render_change_form(request, context, add=add,
# form_url=form_url)
#
# return HttpResponseRedirect(reverse(
# 'admin:{0[0]}_{0[1])_add'.format(info)))
class TabColInline(admin.TabularInline):
extra = 0
classes = ['collapse']
class TaskInline(TabColInline):
model = models.Task
fields = ['title', 'weighting', 'exam', 'hurdle', 'learningoutcomes']
show_change_link=True
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj=None, **kwargs)
outcomes = formset.form.base_fields['learningoutcomes']
outcomes.queryset = models.LearningOutcome.objects.none()
if obj is not None:
outcomes.queryset = models.LearningOutcome.objects.filter(
courseversion_id=obj.courseversion_id
)
return formset
class ScheduleInline(TabColInline):
model = models.Schedule
formfield_overrides = {
TextField: {
'widget': Textarea(attrs={
'rows': 2,
'style': 'vTextArea',
})}
}
class IAInline(TabColInline):
model = models.IndicativeAssessment
fields = [
'short_title',
'indicative_percentage',
'hurdle',
'learningoutcomes',
]
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj=None, **kwargs)
outcomes = formset.form.base_fields['learningoutcomes']
outcomes.queryset = models.LearningOutcome.objects.none()
if obj is not None:
outcomes.queryset = obj.learningoutcome_set.all()
return formset
class LOInline(TabColInline):
model = models.LearningOutcome
filter_horizontal = ['objectives']
# def formfield_for_manytomany(self, db_field, request, **kwargs):
# """Filter available taxonomy(standards) objects to children of those
# linked by the course version.
#
# The course version can be linked to a set of external standards
# "schemes". Restrict the available options to leaf nodes of the
# taxonomy(standards) tree that are descendants of the nodes linked to by
# the course version these learning outcomes are associated with. o_O
# """
# # Make sure we're editing the right fields and objects on the page
# if db_field.name == 'objectives':
# # Work out the ID and get the object whose add/edit page we're on.
# # Django doesn't give us this easily
# resolver = resolve(request.path_info)
# queryset = models.StandardObjective.objects.none()
# if resolver.url_name == 'cims_courseversion_change':
# version = get_object_or_404(models.CourseVersion,
# pk=resolver.args[0])
# queryset = models.StandardObjective.objects.filter(
# standardsframework__courseversion=version,
# )
#
# kwargs['queryset'] = queryset
# return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj=None, **kwargs)
objectives = formset.form.base_fields['objectives']
objectives.queryset = models.StandardObjective.objects.none()
if obj is not None:
objectives.queryset = models.StandardObjective.objects.filter(
standardsframework__courseversion=obj,
)
return formset
class RoleInline(TabColInline):
model = models.Role
class CIInline(TabColInline):
model = models.CourseInstance
exclude = [
'class_number', 'inherent_reqs', 'staff_feedback', 'course_url',
'exams', 'exam_material', 'online_submission', 'late_submission',
'assignment_return', 'reference_requirements', 'resubmission',
'hard_copy_submission', 'other_info',
]
readonly_fields = ['teachingsession', 'delivery_mode',]
can_delete = False
show_change_link=True
@admin.register(models.Course)
class CourseAdmin(admin.ModelAdmin):
list_filter = ('school__course', 'school',)
@admin.register(models.CourseVersion)
class CourseVersionAdmin(VersionAdmin):
fieldsets = (
(None, {
'fields': ('course', 'version_descriptor', 'approval_date',
'approval_desc', 'title', 'description', 'delivery_mode',
'standards_frameworks', 'objectives',
)
}),
('Requisite Statements', {
'fields': ('prerequisites', 'corequisites', 'incompatibles',),
'classes': ('collapse',),
}),
('Misc Statements', {
'fields': ('research_led', 'req_resources',
'additional_costs', 'participation', 'workload',
'prescribed_texts', 'field_trips', 'rec_resources',
'other_info',
),
'classes': ('collapse',),
}),
)
inlines = [LOInline, IAInline, CIInline]
filter_horizontal = ['standards_frameworks', 'objectives']
list_filter = ('course__school', 'course')
#clear_fields = ['id', 'version_descriptor', 'approval_date', 'approval_desc',
# 'objectives']
def get_form(self, request, obj=None, **kwargs):
"""
Available `StandardObjective` objects depend on what
`StandardsFramework` objects have been selected.
"""
form = super().get_form(request, obj, **kwargs)
objectives = form.base_fields['objectives']
objectives.queryset = models.StandardObjective.objects.none()
if obj is not None:
objectives.queryset = models.StandardObjective.objects.filter(
standardsframework__in=obj.standards_frameworks.all()
)
return form
#def get_urls(self):
# def wrap(view):
# def wrapper(*args, **kwargs):
# return self.admin_site.admin_view(view)(*args, **kwargs)
# wrapper.model_admin = self
# return update_wrapper(wrapper, view)
#
# info = self.model._meta.app_label, self.model._meta.model_name
#
# urls = [
# url(r'^copy/$', wrap(self.copy_view),
# name='{0[0]}_{0[1]}_copy'.format(info)),
# ]
# return urls + super().get_urls()
@admin.register(models.CourseInstance)
class CourseInstanceAdmin(VersionAdmin):
fieldsets = (
(None, {
'fields': ('courseversion','instance_descriptor', 'start_date',
'end_date', ('class_number', 'teachingsession',),
'delivery_mode','course_url',
)
}),
('Assignment Submission Statements', {
'fields': ('online_submission', 'late_submission',
'resubmission', 'reference_requirements',
'assignment_return', 'hard_copy_submission',),
'classes': ('collapse',),
}),
('Misc Statements', {
'fields': ('inherent_reqs', 'staff_feedback', 'exams',
'exam_material', 'other_info',
),
'classes': ('collapse',),
}),
)
inlines = [ScheduleInline, TaskInline, RoleInline]
@admin.register(models.LearningOutcome)
class LearningOutcomeAdmin(VersionAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
objectives = form.base_fields['objectives']
objectives.queryset = models.StandardObjective.objects.none()
if obj is not None:
frameworks = obj.courseversion.standards_frameworks.all()
objectives.queryset = models.StandardObjective.objects.filter(
standardsframework__in=frameworks
)
return form
@admin.register(models.IndicativeAssessment)
class IndicativeAssessmentAdmin(VersionAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
outcomes = form.base_fields['learningoutcomes']
outcomes.queryset = models.LearningOutcome.objects.none()
if obj is not None:
outcomes.queryset = models.LearningOutcome.objects.filter(
courseversion_id=obj.courseversion_id
)
return form
@admin.register(models.Task)
class TaskAdmin(VersionAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
outcomes = form.base_fields['learningoutcomes']
outcomes.queryset = models.LearningOutcome.objects.none()
if obj is not None:
instance = obj.courseinstance
outcomes.queryset = models.LearningOutcome.objects.filter(
courseversion_id=instance.courseversion_id
)
return form
#@admin.register(models.Taxonomy)
#class TaxonomyAdmin(MPTTModelAdmin):
# list_display = ('name', 'short_description')
#
# def short_description(self, obj):
# if obj.description:
# return '{}...'.format(obj.description[:40])
# else:
# return ''
@admin.register(models.Schedule)
class ScheduleAdmin(VersionAdmin):
pass
admin.site.register(models.StandardsBody)
admin.site.register(models.StandardsFramework)
admin.site.register(models.StandardObjective)
admin.site.register(models.College)
admin.site.register(models.School)
admin.site.register(models.TeachingSession)
admin.site.register(models.DeliveryMode)
#admin.site.register(models.Person)
admin.site.register(models.Role)
admin.site.unregister(UserModel)
@admin.register(UserModel)
class CIMSUserAdmin(VersionAdmin, UserAdmin):
pass
from django.apps import AppConfig
class CimsConfig(AppConfig):
name = 'cims'
from django.contrib import admin
#from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
#from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import Group
from cims.auth import models
#class CIMSUserCreationForm(UserCreationForm):
#
# class Meta(UserCreationForm.Meta):
# model = models.User
# fields = UserCreationForm.Meta.fields
#
#
#class CIMSUserChangeForm(UserChangeForm):
#
# class Meta(UserChangeForm.Meta):
# model = models.User
# fields = UserChangeForm.Meta.fields
#admin.site.unregister(User)
#@admin.register(models.User)
#class UserAdmin(BaseUserAdmin):
# form = CIMSUserChangeForm
# add_form = CIMSUserCreationForm
@admin.register(models.Role)
class RoleAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions', 'child_roles')
admin.site.unregister(Group)
from django.apps import AppConfig
class CIMSAuthConfig(AppConfig):
name = 'cims.auth'
label = 'cims_auth'
def ready(self):
import cims.auth.signals
super().ready()
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.auth.backends import ModelBackend
from django.contrib.contenttypes.models import ContentType
from perfieldperms.models import PerFieldPermission
from cims.auth.models import Role, UserObjectPermission, RoleObjectPermission
class CIMSBackend(object):
"""
Provide permission checking hooks for PerFieldPermissions.
"""
mbackend = ModelBackend()
def authenticate(self, username=None, password=None, **kwargs):
"""Just use standard ModelBackend for now."""
return self.mbackend.authenticate(username, password, **kwargs)
def get_user(self, user_id):
"""Just use standard ModelBackend for now."""
return self.mbackend.get_user(user_id)
def _cache_to_set(self, cache):
"""Flatten a `cache` dict of sets into a single set."""
perms = set()
perms.update(cache.keys())
perms.update(*cache.values())
return perms
def _merge_caches(self, from_cache, to_cache):
"""
Merge the entries from `from_cache` into `to_cache`. In this case an
empty set overwrites a non-empty set.
"""
for key, value in from_cache.items():