add resource course views
This commit is contained in:
parent
cf31735e80
commit
bc2c64f3b8
11 changed files with 358 additions and 30 deletions
34
dmoj/urls.py
34
dmoj/urls.py
|
@ -492,7 +492,39 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
||||||
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
|
||||||
|
url(
|
||||||
|
r"^courses/(?P<pk>\d+)-(?P<slug>[\w-]*)",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
url(
|
||||||
|
r"^$",
|
||||||
|
course.CourseHome.as_view(),
|
||||||
|
name="course_home",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/resource/$",
|
||||||
|
course.CourseResourceList.as_view(),
|
||||||
|
name="course_resource",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/resource_edit/$",
|
||||||
|
course.CourseResourceEdit.as_view(),
|
||||||
|
name="course_resource_edit",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/resource/(?P<pk>\d+)/$",
|
||||||
|
course.CourseResouceDetail.as_view(),
|
||||||
|
name="course_resource_detail",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/resource/(?P<pk>\d+)/edit",
|
||||||
|
course.CourseResourceDetailEdit.as_view(),
|
||||||
|
name="course_resource_detail_edit",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
||||||
contests.ContestCalendar.as_view(),
|
contests.ContestCalendar.as_view(),
|
||||||
|
|
|
@ -42,6 +42,8 @@ from judge.models import (
|
||||||
Course,
|
Course,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from judge.models.course import CourseResource
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(BlogPost, BlogPostAdmin)
|
admin.site.register(BlogPost, BlogPostAdmin)
|
||||||
admin.site.register(Comment, CommentAdmin)
|
admin.site.register(Comment, CommentAdmin)
|
||||||
|
@ -66,3 +68,4 @@ admin.site.register(Submission, SubmissionAdmin)
|
||||||
admin.site.register(Ticket, TicketAdmin)
|
admin.site.register(Ticket, TicketAdmin)
|
||||||
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
||||||
admin.site.register(Course)
|
admin.site.register(Course)
|
||||||
|
admin.site.register(CourseResource)
|
||||||
|
|
39
judge/migrations/0154_auto_20230301_1659.py
Normal file
39
judge/migrations/0154_auto_20230301_1659.py
Normal file
File diff suppressed because one or more lines are too long
|
@ -58,9 +58,7 @@ class Course(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_editable_by(course, profile):
|
def is_editable_by(cls, course, profile):
|
||||||
if profile.is_superuser:
|
|
||||||
return True
|
|
||||||
userquery = CourseRole.objects.filter(course=course, user=profile)
|
userquery = CourseRole.objects.filter(course=course, user=profile)
|
||||||
if userquery.exists():
|
if userquery.exists():
|
||||||
if userquery[0].role == "AS" or userquery[0].role == "TE":
|
if userquery[0].role == "AS" or userquery[0].role == "TE":
|
||||||
|
@ -68,7 +66,7 @@ class Course(models.Model):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_accessible_by(cls,course, profile):
|
def is_accessible_by(cls, course, profile):
|
||||||
userqueryset = CourseRole.objects.filter(course=course, user=profile)
|
userqueryset = CourseRole.objects.filter(course=course, user=profile)
|
||||||
if userqueryset.exists():
|
if userqueryset.exists():
|
||||||
return True
|
return True
|
||||||
|
@ -76,35 +74,35 @@ class Course(models.Model):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_students(cls,course):
|
def get_students(cls, course):
|
||||||
return CourseRole.objects.filter(course=course, role="ST").values("user")
|
return CourseRole.objects.filter(course=course, role="ST").values("user")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_assistants(cls,course):
|
def get_assistants(cls, course):
|
||||||
return CourseRole.objects.filter(course=course, role="AS").values("user")
|
return CourseRole.objects.filter(course=course, role="AS").values("user")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_teachers(cls,course):
|
def get_teachers(cls, course):
|
||||||
return CourseRole.objects.filter(course=course, role="TE").values("user")
|
return CourseRole.objects.filter(course=course, role="TE").values("user")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_student(cls,course, profiles):
|
def add_student(cls, course, profiles):
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
CourseRole.make_role(course=course, user=profile, role="ST")
|
CourseRole.make_role(course=course, user=profile, role="ST")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_teachers(cls,course, profiles):
|
def add_teachers(cls, course, profiles):
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
CourseRole.make_role(course=course, user=profile, role="TE")
|
CourseRole.make_role(course=course, user=profile, role="TE")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_assistants(cls,course, profiles):
|
def add_assistants(cls, course, profiles):
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
CourseRole.make_role(course=course, user=profile, role="AS")
|
CourseRole.make_role(course=course, user=profile, role="AS")
|
||||||
|
|
||||||
|
|
||||||
class CourseRole(models.Model):
|
class CourseRole(models.Model):
|
||||||
course = models.OneToOneField(
|
course = models.ForeignKey(
|
||||||
Course,
|
Course,
|
||||||
verbose_name=_("course"),
|
verbose_name=_("course"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -142,7 +140,7 @@ class CourseRole(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class CourseResource(models.Model):
|
class CourseResource(models.Model):
|
||||||
course = models.OneToOneField(
|
course = models.ForeignKey(
|
||||||
Course,
|
Course,
|
||||||
verbose_name=_("course"),
|
verbose_name=_("course"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -165,15 +163,18 @@ class CourseResource(models.Model):
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Resource - {self.pk}"
|
||||||
|
|
||||||
|
|
||||||
class CourseAssignment(models.Model):
|
class CourseAssignment(models.Model):
|
||||||
course = models.OneToOneField(
|
course = models.ForeignKey(
|
||||||
Course,
|
Course,
|
||||||
verbose_name=_("course"),
|
verbose_name=_("course"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
contest = models.OneToOneField(
|
contest = models.ForeignKey(
|
||||||
Contest,
|
Contest,
|
||||||
verbose_name=_("contest"),
|
verbose_name=_("contest"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
from django.db import models
|
from django.forms import ModelForm
|
||||||
from judge.models.course import Course
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic import ListView
|
from django.views.generic.edit import UpdateView
|
||||||
|
from judge.models.course import Course, CourseResource
|
||||||
|
from django.views.generic import ListView, UpdateView, DetailView
|
||||||
|
from judge.views.feed import FeedView
|
||||||
|
from django.http import (
|
||||||
|
Http404,
|
||||||
|
HttpResponsePermanentRedirect,
|
||||||
|
HttpResponseRedirect,
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from judge.utils.views import (
|
||||||
|
generic_message,
|
||||||
|
)
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CourseList",
|
"CourseList",
|
||||||
|
@ -13,19 +29,181 @@ __all__ = [
|
||||||
"CourseResourceEdit",
|
"CourseResourceEdit",
|
||||||
]
|
]
|
||||||
|
|
||||||
course_directory_file = ""
|
|
||||||
|
class CourseBase(object):
|
||||||
|
def is_editable_by(self, course=None):
|
||||||
|
if course is None:
|
||||||
|
course = self.object
|
||||||
|
if self.request.profile:
|
||||||
|
return Course.is_editable_by(course, self.request.profile)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_accessible_by(self, course):
|
||||||
|
if course is None:
|
||||||
|
course = self.object
|
||||||
|
if self.request.profile:
|
||||||
|
return Course.is_accessible_by(course, self.request.profile)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class CourseMixin(CourseBase):
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["can_edit"] = self.is_editable_by(self.course)
|
||||||
|
context["can access"] = self.is_accessible_by(self.course)
|
||||||
|
context["course"] = self.course
|
||||||
|
return context
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
print(self)
|
||||||
|
try:
|
||||||
|
self.course_id = int(kwargs["pk"])
|
||||||
|
self.course = get_object_or_404(Course, id=self.course_id)
|
||||||
|
except Http404:
|
||||||
|
key = None
|
||||||
|
if hasattr(self, "slug_url_kwarg"):
|
||||||
|
key = kwargs.get(self.slug_url_kwarg, None)
|
||||||
|
if key:
|
||||||
|
return generic_message(
|
||||||
|
request,
|
||||||
|
_("No such course"),
|
||||||
|
_('Could not find a course with the key "%s".') % key,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return generic_message(
|
||||||
|
request,
|
||||||
|
_("No such course"),
|
||||||
|
_("Could not find such course."),
|
||||||
|
)
|
||||||
|
if self.course.slug != kwargs["slug"]:
|
||||||
|
return HttpResponsePermanentRedirect(
|
||||||
|
request.get_full_path().replace(kwargs["slug"], self.course.slug)
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(CourseMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseHomeView(CourseMixin):
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
if not hasattr(self, "course"):
|
||||||
|
self.course = self.object
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseHome(CourseHomeView, FeedView):
|
||||||
|
template_name = "course/home.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CourseResource.objects.filter(
|
||||||
|
is_public=True,
|
||||||
|
course=self.course,
|
||||||
|
).order_by("order")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseHome, self).get_context_data(**kwargs)
|
||||||
|
context["title"] = self.course.name
|
||||||
|
context["description"] = self.course.about
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseResourceList(CourseMixin, ListView):
|
||||||
|
template_name = "course/resource.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CourseResource.objects.filter(
|
||||||
|
is_public=True,
|
||||||
|
course=self.course,
|
||||||
|
).order_by("order")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseResourceList, self).get_context_data(**kwargs)
|
||||||
|
context["title"] = self.course.name
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseResouceDetail(DetailView):
|
||||||
|
template_name = "course/resource-content.html"
|
||||||
|
model = CourseResource
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseResouceDetail, self).get_context_data(**kwargs)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseAdminMixin(CourseMixin):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
res = super(CourseAdminMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
if not hasattr(self, "course") or self.is_editable_by(self.course):
|
||||||
|
return res
|
||||||
|
return generic_message(
|
||||||
|
request,
|
||||||
|
_("Can't edit course"),
|
||||||
|
_("You are not allowed to edit this course."),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseResourceDetailEditForm(ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CourseResourceDetailEditForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseResourceDetailEdit(LoginRequiredMixin, UpdateView):
|
||||||
|
template_name = "course/resource_detail_edit.html"
|
||||||
|
model = CourseResource
|
||||||
|
fields = ["description", "files", "is_public"]
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.get_full_path()
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseResourceEdit(CourseMixin, LoginRequiredMixin, ListView):
|
||||||
|
template_name = "course/resource_edit.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CourseResource.objects.filter(
|
||||||
|
course=self.course,
|
||||||
|
).order_by("order")
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
for resource in queryset:
|
||||||
|
if request.POST.get("resource-" + str(resource.pk) + "-delete") != None:
|
||||||
|
resource.delete()
|
||||||
|
else:
|
||||||
|
if request.POST.get("resource-" + str(resource.pk) + "-public") != None:
|
||||||
|
resource.is_public = True
|
||||||
|
else:
|
||||||
|
resource.is_public = False
|
||||||
|
resource.order = request.POST.get(
|
||||||
|
"resource-" + str(resource.pk) + "-order"
|
||||||
|
)
|
||||||
|
resource.save()
|
||||||
|
return HttpResponseRedirect(request.path)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super(CourseResourceEdit, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CourseListMixin(object):
|
class CourseListMixin(object):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Course.objects.filter(is_open = "true").values()
|
return Course.objects.filter(is_open="true").values()
|
||||||
|
|
||||||
|
|
||||||
class CourseList(ListView):
|
class CourseList(ListView):
|
||||||
model = Course
|
model = Course
|
||||||
template_name = "course/list.html"
|
template_name = "course/list.html"
|
||||||
queryset = Course.objects.filter(is_public=True).filter(is_open=True)
|
queryset = Course.objects.filter(is_public=True).filter(is_open=True)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CourseList,self).get_context_data(**kwargs)
|
context = super(CourseList, self).get_context_data(**kwargs)
|
||||||
available , enrolling = [] , []
|
available, enrolling = [], []
|
||||||
for course in Course.objects.filter(is_public=True).filter(is_open=True):
|
for course in Course.objects.filter(is_public=True).filter(is_open=True):
|
||||||
if Course.is_accessible_by(course, self.request.profile):
|
if Course.is_accessible_by(course, self.request.profile):
|
||||||
enrolling.append(course)
|
enrolling.append(course)
|
||||||
|
@ -34,4 +212,3 @@ class CourseList(ListView):
|
||||||
context["available"] = available
|
context["available"] = available
|
||||||
context["enrolling"] = enrolling
|
context["enrolling"] = enrolling
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
4
templates/course/home.html
Normal file
4
templates/course/home.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<h1> {{ title }} - Home </h1>
|
||||||
|
<h2> About: {{ description }} </h2>
|
||||||
|
|
||||||
|
<a href="/courses/{{ course.pk }}-{{ course.slug }}/resource"> See course resource.</a>
|
|
@ -1,19 +1,30 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Enrolling</h1>
|
<h1>Enrolling</h1>
|
||||||
{% for course in enrolling %}
|
<ul>
|
||||||
<h2> {{ course }} </h2>
|
{% for course in enrolling %}
|
||||||
{% endfor %}
|
<li>
|
||||||
|
<a href="/courses/{{ course.pk }}-{{ course.slug }}"> {{ course }} </a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
<h1> Available </h1>
|
<h1> Available </h1>
|
||||||
{% for course in available %}
|
<ul>
|
||||||
<h2> {{ course }} </h2>
|
{% for course in available %}
|
||||||
{% endfor %}
|
<li>
|
||||||
|
<a href="/courses/{{ course.pk }}-{{ course.slug }}"> {{ course }} </a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
12
templates/course/resource-content.html
Normal file
12
templates/course/resource-content.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<h1> {{ object }} </h1>
|
||||||
|
<h2> This resource belong to {{ object.course }}</h2>
|
||||||
|
|
||||||
|
<h3> {{ object.description }}</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if object.files != None %}
|
||||||
|
<div> <a href="{{ object.files.url}}" target="_blank"> See files </a> </div>
|
||||||
|
{% endif %}
|
||||||
|
<div> <a href="/courses/{{ object.course.pk }}-{{ object.course.slug }}/resource/{{ object.pk }}/edit"> Edit
|
||||||
|
Resource </a> </div>
|
||||||
|
</div>
|
11
templates/course/resource.html
Normal file
11
templates/course/resource.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<h1> {{ course.name }}'s Resource </h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for resource in object_list %}
|
||||||
|
<li>
|
||||||
|
<a href="/courses/{{ course.pk }}-{{ course.slug }}/resource/{{ resource.pk }}"> {{ resource }} </a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href="/courses/{{ course.pk }}-{{ course.slug }}/resource_edit"> Edit resources</a>
|
5
templates/course/resource_detail_edit.html
Normal file
5
templates/course/resource_detail_edit.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="Update">
|
||||||
|
</form>
|
33
templates/course/resource_edit.html
Normal file
33
templates/course/resource_edit.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{{ course }}
|
||||||
|
|
||||||
|
<form id="resource-form" method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
<th>Order</th>
|
||||||
|
<th>Public</th>
|
||||||
|
<th>Edit link</th>
|
||||||
|
</tr>
|
||||||
|
{% for resource in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td> {{ resource }} </td>
|
||||||
|
<td> <input type="checkbox" name="resource-{{ resource.pk }}-delete" id="resource-{{ resource.pk }}-delete">
|
||||||
|
</td>
|
||||||
|
<td> <input type="number" name="resource-{{ resource.pk }}-order" value="{{
|
||||||
|
resource.order }}" id="resource-{{ resource.pk }}-order"> </a></td>
|
||||||
|
{% if resource.is_public %}
|
||||||
|
<td> <input type="checkbox" name="resource-{{ resource.pk }}-public" id="resource-{{ resource.pk }}-public"
|
||||||
|
checked=""> </a></td>
|
||||||
|
{% else %}
|
||||||
|
<td> <input type="checkbox" name="resource-{{ resource.pk }}-public" id="resource-{{ resource.pk }}-public">
|
||||||
|
</a></td>
|
||||||
|
{% endif %}
|
||||||
|
<td> <a href="/courses/{{ course.pk }}-{{ course.slug }}/resource/{{ resource.pk }}/edit"> Edit </a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
Loading…
Reference in a new issue