Commit 10d23f977451c58c9539655845b3425036f3ad29

Authored by Felipe Henrique de Almeida Bormann
2 parents dc825f9a 4536c27f

Merge branch 'refactoring' of https://github.com/amadeusproject/amadeuslms into refactoring

amadeus/settings.py
... ... @@ -58,6 +58,7 @@ INSTALLED_APPS = [
58 58 'log',
59 59 'categories',
60 60 'subjects',
  61 + 'students_group',
61 62 'topics',
62 63 'mailsender',
63 64 'security',
... ...
amadeus/static/css/base/amadeus.css
... ... @@ -73,7 +73,7 @@ a:focus {
73 73 /* side bar menu ends*/
74 74  
75 75 /* category app starts */
76   -.category-panel > .panel-heading, .subject-panel > .panel-heading, .special-panel > .panel-heading, .topic-panel > .panel-heading {
  76 +.category-panel > .panel-heading, .subject-panel > .panel-heading, .special-panel > .panel-heading, .topic-panel > .panel-heading, .group-panel > .panel-heading {
77 77 padding: 2px 0px;
78 78 }
79 79  
... ...
amadeus/static/css/themes/green.css
... ... @@ -59,6 +59,19 @@ a, a:focus, a:hover {
59 59 background-color: #039BE5 !important;
60 60 }
61 61  
  62 +.group-panel > .panel-heading {
  63 + background-color: #FFFFFF !important;
  64 +}
  65 +
  66 +.group-panel .panel-title, .group-panel .category-header i, .group-panel .category-course-link {
  67 + color: #000000 !important;
  68 +}
  69 +
  70 +.group-panel-body {
  71 + background: #FFFFFF !important;
  72 + border-top: 1px solid #CCCCCC;
  73 +}
  74 +
62 75 .topic-panel > .panel-heading {
63 76 background-color: #7BA5B9 !important;
64 77 }
... ...
amadeus/static/js/course.js
... ... @@ -193,4 +193,14 @@ $('.collapse').on('hide.bs.collapse', function (e) {
193 193 });
194 194 }
195 195 }
196   -});
197 196 \ No newline at end of file
  197 +});
  198 +
  199 +function delete_group(url) {
  200 + $('.modal').remove();
  201 +
  202 + $.get(url, function (modal) {
  203 + $("#group-accordion").after(modal);
  204 +
  205 + $('.modal').modal('show');
  206 + });
  207 +}
198 208 \ No newline at end of file
... ...
amadeus/urls.py
... ... @@ -27,6 +27,7 @@ urlpatterns = [
27 27 url(r'^$', index, name = 'home'),
28 28 url(r'^categories/', include('categories.urls', namespace = 'categories')),
29 29 url(r'^subjects/', include('subjects.urls', namespace = 'subjects')),
  30 + url(r'^groups/', include('students_group.urls', namespace = 'groups')),
30 31 url(r'^topics/', include('topics.urls', namespace = 'topics')),
31 32 url(r'^mailsender/', include('mailsender.urls', namespace = 'mailsender')),
32 33 url(r'^security/', include('security.urls', namespace = 'security')),
... ...
categories/migrations/0007_auto_20170118_1711.py 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +# -*- coding: utf-8 -*-
  2 +# Generated by Django 1.10 on 2017-01-18 20:11
  3 +from __future__ import unicode_literals
  4 +
  5 +from django.conf import settings
  6 +from django.db import migrations, models
  7 +
  8 +
  9 +class Migration(migrations.Migration):
  10 +
  11 + dependencies = [
  12 + ('categories', '0006_auto_20170102_1856'),
  13 + ]
  14 +
  15 + operations = [
  16 + migrations.AlterField(
  17 + model_name='category',
  18 + name='coordinators',
  19 + field=models.ManyToManyField(blank=True, related_name='Coordenadores', to=settings.AUTH_USER_MODEL),
  20 + ),
  21 + ]
... ...
students_group/__init__.py 0 → 100644
students_group/admin.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +from django.contrib import admin
  2 +
  3 +# Register your models here.
... ...
students_group/apps.py 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +from django.apps import AppConfig
  2 +
  3 +
  4 +class StudentsGroupConfig(AppConfig):
  5 + name = 'students_group'
... ...
students_group/forms.py 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +# coding=utf-8
  2 +from django import forms
  3 +from django.utils.translation import ugettext_lazy as _
  4 +
  5 +from subjects.models import Subject
  6 +
  7 +from .models import StudentsGroup
  8 +
  9 +class StudentsGroupForm(forms.ModelForm):
  10 + subject = None
  11 +
  12 + def __init__(self, *args, **kwargs):
  13 + super(StudentsGroupForm, self).__init__(*args, **kwargs)
  14 +
  15 + self.subject = kwargs['initial'].get('subject', None)
  16 +
  17 + if self.instance.id:
  18 + self.subject = self.instance.subject
  19 +
  20 + self.fields['participants'].queryset = self.subject.students.all()
  21 +
  22 + def clean_name(self):
  23 + name = self.cleaned_data.get('name', '')
  24 +
  25 + if self.instance.id:
  26 + same_name = self.subject.group_subject.filter(name__unaccent__iexact = name).exclude(id = self.instance.id).count()
  27 + else:
  28 + same_name = self.subject.group_subject.filter(name__unaccent__iexact = name).count()
  29 +
  30 + if same_name > 0:
  31 + self._errors['name'] = [_('This subject already has a group with this name')]
  32 +
  33 + return ValueError
  34 +
  35 + return name
  36 +
  37 + class Meta:
  38 + model = StudentsGroup
  39 + fields = ['name', 'description', 'participants']
  40 + widgets = {
  41 + 'description': forms.Textarea,
  42 + 'participants': forms.SelectMultiple,
  43 + }
0 44 \ No newline at end of file
... ...
students_group/migrations/0001_initial.py 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +# -*- coding: utf-8 -*-
  2 +# Generated by Django 1.10 on 2017-01-18 20:11
  3 +from __future__ import unicode_literals
  4 +
  5 +import autoslug.fields
  6 +from django.conf import settings
  7 +from django.db import migrations, models
  8 +import django.db.models.deletion
  9 +
  10 +
  11 +class Migration(migrations.Migration):
  12 +
  13 + initial = True
  14 +
  15 + dependencies = [
  16 + migrations.swappable_dependency(settings.AUTH_USER_MODEL),
  17 + ('subjects', '0012_auto_20170112_1408'),
  18 + ]
  19 +
  20 + operations = [
  21 + migrations.CreateModel(
  22 + name='StudentsGroup',
  23 + fields=[
  24 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  25 + ('name', models.CharField(max_length=200, verbose_name='Name')),
  26 + ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True, verbose_name='Slug')),
  27 + ('description', models.TextField(blank=True, verbose_name='Description')),
  28 + ('create_date', models.DateTimeField(auto_now_add=True, verbose_name='Create Date')),
  29 + ('last_update', models.DateTimeField(auto_now=True, verbose_name='Last Update')),
  30 + ('participants', models.ManyToManyField(blank=True, related_name='group_participants', to=settings.AUTH_USER_MODEL, verbose_name='Participants')),
  31 + ('subject', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='group_subject', to='subjects.Subject', verbose_name='Subject')),
  32 + ],
  33 + options={
  34 + 'verbose_name': 'Students Group',
  35 + 'verbose_name_plural': 'Students Groups',
  36 + },
  37 + ),
  38 + ]
... ...
students_group/migrations/0002_auto_20170118_1800.py 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +# -*- coding: utf-8 -*-
  2 +# Generated by Django 1.10 on 2017-01-18 21:00
  3 +from __future__ import unicode_literals
  4 +
  5 +from django.db import migrations
  6 +
  7 +from django.contrib.postgres.operations import UnaccentExtension
  8 +
  9 +class Migration(migrations.Migration):
  10 +
  11 + dependencies = [
  12 + ('students_group', '0001_initial'),
  13 + ]
  14 +
  15 + operations = [
  16 + UnaccentExtension()
  17 + ]
... ...
students_group/migrations/__init__.py 0 → 100644
students_group/models.py 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +from django.db import models
  2 +from autoslug.fields import AutoSlugField
  3 +from django.utils.translation import ugettext_lazy as _
  4 +
  5 +from subjects.models import Subject
  6 +from users.models import User
  7 +
  8 +class StudentsGroup(models.Model):
  9 + name = models.CharField(_('Name'), max_length = 200)
  10 + slug = AutoSlugField(_("Slug"), populate_from = 'name', unique = True)
  11 + description = models.TextField(_('Description'), blank = True)
  12 + subject = models.ForeignKey(Subject, verbose_name = _('Subject'), related_name = 'group_subject', null = True)
  13 + participants = models.ManyToManyField(User, verbose_name = _('Participants'), related_name = 'group_participants', blank = True)
  14 + create_date = models.DateTimeField(_('Create Date'), auto_now_add = True)
  15 + last_update = models.DateTimeField(_('Last Update'), auto_now = True)
  16 +
  17 + class Meta:
  18 + verbose_name = _('Students Group')
  19 + verbose_name_plural = _('Students Groups')
  20 + ordering = ['name']
  21 +
  22 + def __str__(self):
  23 + return self.name
0 24 \ No newline at end of file
... ...
students_group/templates/groups/_form.html 0 → 100644
... ... @@ -0,0 +1,113 @@
  1 +{% load widget_tweaks static i18n %}
  2 +
  3 +<form method="post" action="" enctype="multipart/form-data">
  4 + {% csrf_token %}
  5 + {% for field in form %}
  6 + {% if field.auto_id == 'id_participants' %}
  7 + <div class="panel-group" id="professors_accordion" role="tablist" aria-multiselectable="true">
  8 + <div class="panel panel-info">
  9 + <div class="panel-heading">
  10 + <div class="row">
  11 + <div class="col-md-12">
  12 + <a data-parent="#professors_accordion" data-toggle="collapse" href="#participants">
  13 + <h4 class="panel-title">
  14 + <button class="btn btn-default btn-xs text-center cat-selector"><i class="fa fa-angle-right fa-2x" aria-hidden="true"></i></button><label for="{{ field.auto_id }}">{{ field.label }}</label>
  15 + </h4>
  16 + </a>
  17 + </div>
  18 + </div>
  19 + </div>
  20 + <div id="participants" class="panel-collapse collapse">
  21 + <p><em>{% trans 'Attribute students to group' %}:</em></p>
  22 + {% render_field field class='form-control' %}
  23 + </div>
  24 + </div>
  25 + </div>
  26 + {% else %}
  27 + <div class="form-group {% if form.has_error %} has-error {% endif %} is-fileinput">
  28 + <label for="{{ field.auto_id }}" class="control-label">{{ field.label }}</label>
  29 +
  30 + {% if field.auto_id == 'id_description' %}
  31 + {% render_field field class='form-control text_wysiwyg' %}
  32 + {% else %}
  33 + {% render_field field class='form-control' %}
  34 + {% endif %}
  35 + </div>
  36 + {% endif %}
  37 +
  38 + <span class="help-block">{{ field.help_text }}</span>
  39 +
  40 + {% if field.errors %}
  41 + <div class="row">
  42 + </br>
  43 + <div class="alert alert-danger alert-dismissible" role="alert">
  44 + <button type="button" class="close" data-dismiss="alert" aria-label="Close">
  45 + <span aria-hidden="true">&times;</span>
  46 + </button>
  47 + <ul>
  48 + {% for error in field.errors %}
  49 + <li>{{ error }}</li>
  50 + {% endfor %}
  51 + </ul>
  52 + </div>
  53 + </div>
  54 + {% endif %}
  55 + {% endfor %}
  56 + <div class="row text-center">
  57 + <input type="submit" value="{% trans 'Save' %}" class="btn btn-success btn-raised" />
  58 + </div>
  59 +</form>
  60 +
  61 +<script type="text/javascript">
  62 + $('#id_participants').multiSelect({
  63 + selectableHeader: "<input type='text' class='search-input category-search-users' autocomplete='off' placeholder=' '>",
  64 + selectionHeader: "<input type='text' class='search-input category-search-users' autocomplete='off' placeholder=''>",
  65 + afterInit: function(ms){
  66 + var that = this,
  67 + $selectableSearch = that.$selectableUl.prev(),
  68 + $selectionSearch = that.$selectionUl.prev(),
  69 + selectableSearchString = '#'+that.$container.attr('id')+' .ms-elem-selectable:not(.ms-selected)',
  70 + selectionSearchString = '#'+that.$container.attr('id')+' .ms-elem-selection.ms-selected';
  71 +
  72 + that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
  73 + .on('keydown', function(e){
  74 + if (e.which === 40){
  75 + that.$selectableUl.focus();
  76 + return false;
  77 + }
  78 + });
  79 +
  80 + that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
  81 + .on('keydown', function(e){
  82 + if (e.which == 40){
  83 + that.$selectionUl.focus();
  84 + return false;
  85 + }
  86 + });
  87 + },
  88 + afterSelect: function(){
  89 + this.qs1.cache();
  90 + this.qs2.cache();
  91 + },
  92 + afterDeselect: function(){
  93 + this.qs1.cache();
  94 + this.qs2.cache();
  95 + }
  96 + });// Used to create multi-select css style
  97 +
  98 + $('.collapse').on('show.bs.collapse', function (e) {
  99 + if($(this).is(e.target)){
  100 + var btn = $(this).parent().find('.fa-angle-right');
  101 +
  102 + btn.switchClass("fa-angle-right", "fa-angle-down", 250, "easeInOutQuad");
  103 + }
  104 + });
  105 +
  106 + $('.collapse').on('hide.bs.collapse', function (e) {
  107 + if($(this).is(e.target)){
  108 + var btn = $(this).parent().find('.fa-angle-down');
  109 +
  110 + btn.switchClass("fa-angle-down", "fa-angle-right", 250, "easeInOutQuad");
  111 + }
  112 + });
  113 +</script>
0 114 \ No newline at end of file
... ...
students_group/templates/groups/create.html 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +{% extends 'groups/index.html' %}
  2 +
  3 +{% load i18n django_bootstrap_breadcrumbs %}
  4 +
  5 +{% block breadcrumbs %}
  6 + {{ block.super }}
  7 +
  8 + {% if group %}
  9 + {% trans 'Replicate: ' as bread %}
  10 +
  11 + {% with bread|add:group.name as bread_slug %}
  12 + {% breadcrumb bread_slug 'groups:replicate' subject.slug group.slug %}
  13 + {% endwith %}
  14 + {% else %}
  15 + {% trans 'Create Group' as bread %}
  16 + {% breadcrumb bread 'groups:create' subject.slug %}
  17 + {% endif %}
  18 +{% endblock %}
  19 +
  20 +{% block content %}
  21 + <div class="card">
  22 + <div class="card-content">
  23 + <div class="card-body">
  24 + {% include 'groups/_form.html' %}
  25 + </div>
  26 + </div>
  27 + </div>
  28 + <br clear="all" />
  29 + <br clear="all" />
  30 +{% endblock %}
... ...
students_group/templates/groups/delete.html 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +{% load i18n %}
  2 +
  3 +<div class="modal fade" id="topic" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
  4 + <div class="modal-dialog" role="document">
  5 + <div class="modal-content">
  6 + <div class="modal-body">
  7 + <form id="delete_form" action="{% url 'groups:delete' group.slug %}" method="post">
  8 + {% csrf_token %}
  9 + <h3>{% trans 'Are you sure you want delete the group' %}: {{ group }}?</h3>
  10 + <p>{% trans 'With this action the group will no longer exist but your participants will still be attached to the subject.' %}</p>
  11 + </form>
  12 + </div>
  13 + <div class="modal-footer">
  14 + <div class="pull-right">
  15 + <button type="button" class="btn btn-default btn-raised" data-dismiss="modal">{% trans "Close" %}</button>
  16 + </div>
  17 + <div class="pull-left">
  18 + <button type="submit" form="delete_form" class="btn btn-success btn-raised">{% trans "Delete" %}</button>
  19 + </div>
  20 + </div>
  21 + </div>
  22 + </div>
  23 +</div>
0 24 \ No newline at end of file
... ...
students_group/templates/groups/index.html 0 → 100644
... ... @@ -0,0 +1,89 @@
  1 +{% extends 'subjects/view.html' %}
  2 +
  3 +{% load static i18n pagination %}
  4 +{% load django_bootstrap_breadcrumbs %}
  5 +
  6 +{% block javascript%}
  7 + {{ block.super }}
  8 +{% endblock%}
  9 +
  10 +{% block breadcrumbs %}
  11 + {{ block.super }}
  12 +
  13 + {% trans 'Groups' as bread %}
  14 + {% breadcrumb bread 'groups:index' subject.slug %}
  15 +{% endblock %}
  16 +
  17 +{% block content %}
  18 + {% if messages %}
  19 + {% for message in messages %}
  20 + <div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
  21 + <button type="button" class="close" data-dismiss="alert" aria-label="Close">
  22 + <span aria-hidden="true">&times;</span>
  23 + </button>
  24 + <p>{{ message }}</p>
  25 + </div>
  26 + {% endfor %}
  27 + {% endif %}
  28 +
  29 + <div class='row'>
  30 + <div class="col-md-12 text-center">
  31 + <a href="{% url 'groups:create' subject.slug %}" class="btn btn-raised btn-success">{% trans "Create Group" %}</a>
  32 + </div>
  33 + </div>
  34 +
  35 + <div class="col-md-12 cards-content">
  36 + <div class="panel-group" id="group-accordion" role="tablist" aria-multiselectable="true">
  37 + {% for group in groups %}
  38 + <div class="panel panel-info group-panel">
  39 + <div class="panel-heading">
  40 + <div class="row">
  41 + <div class="col-md-12 category-header">
  42 + <h4 class="panel-title">
  43 + <a class="category-course-link pull-left" data-parent="#group-accordion" data-toggle="collapse" href="#{{group.slug}}">
  44 + <button class="btn btn-default btn-xs text-center cat-selector"><i class="fa fa-angle-right fa-2x" aria-hidden="true"></i></button> {{ group }} ({{ group.participants.count }})
  45 + </a>
  46 + </h4>
  47 +
  48 + <div class="col-md-5 pull-right category-card-items">
  49 + <a href="" id="moreActions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
  50 + <i class="fa fa-ellipsis-v" aria-hidden="true"></i>
  51 + </a>
  52 + <ul class="dropdown-menu pull-right" aria-labelledby="moreActions">
  53 + <li>
  54 + <a href="{% url 'groups:replicate' subject.slug group.slug %}">
  55 + <i class="fa fa-files-o" aria-hidden="true"></i> {% trans 'Replicate' %}
  56 + </a>
  57 + </li>
  58 + <li><a href="{% url 'groups:update' subject.slug group.slug %}"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i> {% trans 'Edit' %}</a></li>
  59 + <li><a href="javascript:delete_group('{% url 'groups:delete' group.slug %}')"><i class="fa fa-trash fa-fw" aria-hidden="true"></i> {% trans 'Remove' %}</a></li>
  60 + </ul>
  61 + </div>
  62 + </div>
  63 + </div>
  64 + </div>
  65 + <div id="{{ group.slug }}" class="panel-collapse panel-body collapse category-panel-content group-panel-body">
  66 + {% autoescape off %}
  67 + {{ group.description }}
  68 + {% endautoescape %}
  69 +
  70 + <h3>{% trans 'Participant(s)' %}: </h3>
  71 +
  72 + <ul class="list-inline">
  73 + {% for user in group.participants.all %}
  74 + <li class="list-inline-item text-center col-lg-2 col-md-2 col-sm-2 col-xs-3">
  75 + <img src="{{ user.image_url }}" class="img-responsive" />
  76 + <p><small>{{ user }}</small></p>
  77 + </li>
  78 + {% endfor %}
  79 + </ul>
  80 + </div>
  81 + </div>
  82 + {% endfor %}
  83 +
  84 + {% pagination request paginator page_obj %}
  85 + </div>
  86 + </div>
  87 +
  88 + <script type="text/javascript" src="{% static 'js/course.js' %}"></script>
  89 +{% endblock %}
0 90 \ No newline at end of file
... ...
students_group/templates/groups/update.html 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +{% extends 'groups/index.html' %}
  2 +
  3 +{% load i18n django_bootstrap_breadcrumbs %}
  4 +
  5 +{% block breadcrumbs %}
  6 + {{ block.super }}
  7 +
  8 + {% trans 'Update: ' as bread %}
  9 +
  10 + {% with bread|add:group.name as bread_slug %}
  11 + {% breadcrumb bread_slug 'groups:update' subject.slug group.slug %}
  12 + {% endwith %}
  13 +{% endblock %}
  14 +
  15 +{% block content %}
  16 + <div class="card">
  17 + <div class="card-content">
  18 + <div class="card-body">
  19 + {% include 'groups/_form.html' %}
  20 + </div>
  21 + </div>
  22 + </div>
  23 + <br clear="all" />
  24 + <br clear="all" />
  25 +{% endblock %}
... ...
students_group/tests.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +from django.test import TestCase
  2 +
  3 +# Create your tests here.
... ...
students_group/urls.py 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +from django.conf.urls import url
  2 +from . import views
  3 +
  4 +urlpatterns = [
  5 + url(r'^(?P<slug>[\w_-]+)/$', views.IndexView.as_view(), name='index'),
  6 + url(r'^create/(?P<slug>[\w_-]+)/$', views.CreateView.as_view(), name='create'),
  7 + url(r'^update/(?P<sub_slug>[\w_-]+)/(?P<slug>[\w_-]+)/$', views.UpdateView.as_view(), name='update'),
  8 + url(r'^replicate/(?P<slug>[\w_-]+)/(?P<group_slug>[\w_-]+)/$', views.CreateView.as_view(), name='replicate'),
  9 + url(r'^delete/(?P<slug>[\w_-]+)/$', views.DeleteView.as_view(), name = 'delete'),
  10 +]
0 11 \ No newline at end of file
... ...
students_group/views.py 0 → 100644
... ... @@ -0,0 +1,174 @@
  1 +from django.shortcuts import get_object_or_404, redirect, render
  2 +from django.views import generic
  3 +from django.contrib import messages
  4 +from django.http import JsonResponse
  5 +from django.core.urlresolvers import reverse, reverse_lazy
  6 +from django.utils.translation import ugettext_lazy as _
  7 +from django.contrib.auth.mixins import LoginRequiredMixin
  8 +
  9 +from amadeus.permissions import has_subject_permissions
  10 +
  11 +from subjects.models import Subject
  12 +
  13 +from .models import StudentsGroup
  14 +from .forms import StudentsGroupForm
  15 +
  16 +class IndexView(LoginRequiredMixin, generic.ListView):
  17 + login_url = reverse_lazy("users:login")
  18 + redirect_field_name = 'next'
  19 +
  20 + model = StudentsGroup
  21 + context_object_name = 'groups'
  22 + template_name = 'groups/index.html'
  23 + paginate_by = 10
  24 +
  25 + def dispatch(self, request, *args, **kwargs):
  26 + slug = self.kwargs.get('slug', '')
  27 + subject = get_object_or_404(Subject, slug = slug)
  28 +
  29 + if not has_subject_permissions(request.user, subject):
  30 + return redirect(reverse_lazy('subjects:home'))
  31 +
  32 + return super(IndexView, self).dispatch(request, *args, **kwargs)
  33 +
  34 + def get_queryset(self):
  35 + slug = self.kwargs.get('slug', '')
  36 + subject = get_object_or_404(Subject, slug = slug)
  37 +
  38 + return StudentsGroup.objects.filter(subject = subject)
  39 +
  40 + def get_context_data(self, **kwargs):
  41 + context = super(IndexView, self).get_context_data(**kwargs)
  42 +
  43 + slug = self.kwargs.get('slug', '')
  44 + subject = get_object_or_404(Subject, slug = slug)
  45 +
  46 + context['title'] = _('Students Groups')
  47 + context['subject'] = subject
  48 +
  49 + return context
  50 +
  51 +class CreateView(LoginRequiredMixin, generic.edit.CreateView):
  52 + login_url = reverse_lazy("users:login")
  53 + redirect_field_name = 'next'
  54 +
  55 + template_name = 'groups/create.html'
  56 + form_class = StudentsGroupForm
  57 +
  58 + def dispatch(self, request, *args, **kwargs):
  59 + slug = self.kwargs.get('slug', '')
  60 + subject = get_object_or_404(Subject, slug = slug)
  61 +
  62 + if not has_subject_permissions(request.user, subject):
  63 + return redirect(reverse_lazy('subjects:home'))
  64 +
  65 + return super(CreateView, self).dispatch(request, *args, **kwargs)
  66 +
  67 + def get_initial(self):
  68 + initial = super(CreateView, self).get_initial()
  69 +
  70 + slug = self.kwargs.get('slug', '')
  71 +
  72 + initial['subject'] = get_object_or_404(Subject, slug = slug)
  73 +
  74 + if self.kwargs.get('group_slug'):
  75 + group = get_object_or_404(StudentsGroup, slug = self.kwargs['group_slug'])
  76 + initial = initial.copy()
  77 + initial['description'] = group.description
  78 + initial['name'] = group.name
  79 + initial['participants'] = group.participants.all()
  80 +
  81 + return initial
  82 +
  83 + def form_valid(self, form):
  84 + self.object = form.save(commit = False)
  85 +
  86 + slug = self.kwargs.get('slug', '')
  87 + subject = get_object_or_404(Subject, slug = slug)
  88 +
  89 + self.object.subject = subject
  90 +
  91 + self.object.save()
  92 +
  93 + return super(CreateView, self).form_valid(form)
  94 +
  95 + def get_context_data(self, **kwargs):
  96 + context = super(CreateView, self).get_context_data(**kwargs)
  97 +
  98 + context['title'] = _('Create Topic')
  99 +
  100 + slug = self.kwargs.get('slug', '')
  101 + subject = get_object_or_404(Subject, slug = slug)
  102 +
  103 + context['subject'] = subject
  104 +
  105 + if self.kwargs.get('group_slug'):
  106 + group = get_object_or_404(StudentsGroup, slug = self.kwargs['group_slug'])
  107 +
  108 + context['title'] = _('Replicate Group')
  109 +
  110 + context['group'] = group
  111 +
  112 + return context
  113 +
  114 + def get_success_url(self):
  115 + messages.success(self.request, _('The group "%s" was created successfully!')%(self.object.name))
  116 +
  117 + return reverse_lazy('groups:index', kwargs = {'slug': self.object.subject.slug})
  118 +
  119 +class UpdateView(LoginRequiredMixin, generic.UpdateView):
  120 + login_url = reverse_lazy("users:login")
  121 + redirect_field_name = 'next'
  122 +
  123 + template_name = 'groups/update.html'
  124 + model = StudentsGroup
  125 + form_class = StudentsGroupForm
  126 + context_object_name = 'group'
  127 +
  128 + def dispatch(self, request, *args, **kwargs):
  129 + slug = self.kwargs.get('sub_slug', '')
  130 + subject = get_object_or_404(Subject, slug = slug)
  131 +
  132 + if not has_subject_permissions(request.user, subject):
  133 + return redirect(reverse_lazy('subjects:home'))
  134 +
  135 + return super(UpdateView, self).dispatch(request, *args, **kwargs)
  136 +
  137 + def get_context_data(self, **kwargs):
  138 + context = super(UpdateView, self).get_context_data(**kwargs)
  139 +
  140 + context['title'] = _('Update Group')
  141 +
  142 + slug = self.kwargs.get('sub_slug', '')
  143 + subject = get_object_or_404(Subject, slug = slug)
  144 +
  145 + context['subject'] = subject
  146 +
  147 + return context
  148 +
  149 + def get_success_url(self):
  150 + messages.success(self.request, _('The group "%s" was updated successfully!')%(self.object.name))
  151 +
  152 + return reverse_lazy('groups:index', kwargs = {'slug': self.object.subject.slug})
  153 +
  154 +class DeleteView(LoginRequiredMixin, generic.DeleteView):
  155 + login_url = reverse_lazy("users:login")
  156 + redirect_field_name = 'next'
  157 +
  158 + template_name = 'groups/delete.html'
  159 + model = StudentsGroup
  160 + context_object_name = 'group'
  161 +
  162 + def dispatch(self, request, *args, **kwargs):
  163 + slug = self.kwargs.get('slug', '')
  164 + group = get_object_or_404(StudentsGroup, slug = slug)
  165 +
  166 + if not has_subject_permissions(request.user, group.subject):
  167 + return redirect(reverse_lazy('subjects:home'))
  168 +
  169 + return super(DeleteView, self).dispatch(request, *args, **kwargs)
  170 +
  171 + def get_success_url(self):
  172 + messages.success(self.request, _('The group "%s" was removed successfully!')%(self.object.name))
  173 +
  174 + return reverse_lazy('groups:index', kwargs = {'slug': self.object.subject.slug})
0 175 \ No newline at end of file
... ...
subjects/templates/subjects/list.html
... ... @@ -90,6 +90,7 @@
90 90 <div id="{{category.slug}}" class="panel-collapse panel-body collapse category-panel-content">
91 91 <input type="hidden" class="log_url" value="{% url 'categories:view_log' category.id %}" />
92 92 <input type="hidden" class="log_id" value="" />
  93 +
93 94 {% if category.coordinators.all|length > 0 %}
94 95 <h4><b>{% trans "Coordinator(s) " %}: </b>
95 96 {{ category.coordinators.all|join:', ' }}
... ... @@ -193,4 +194,17 @@
193 194 <div id="modal_subject"></div>
194 195  
195 196 <script type="text/javascript" src="{% static 'js/course.js' %}"></script>
  197 + <script type="text/javascript">
  198 + $(function (){
  199 + var cat_slug = "{{ cat_slug }}"
  200 +
  201 + if (cat_slug != "") {
  202 + $("#" + cat_slug).collapse('show');
  203 +
  204 + $('html, body').animate({
  205 + scrollTop: $("#" + cat_slug).parent().offset().top
  206 + }, 2000);
  207 + }
  208 + });
  209 + </script>
196 210 {% endblock %}
... ...
subjects/templates/subjects/subject_card.html
... ... @@ -18,6 +18,7 @@
18 18 <ul class="dropdown-menu pull-right" aria-labelledby="moreActions">
19 19 <li><a href="{% url 'subjects:replicate' subject.slug %}"><i class="fa fa-files-o fa-fw" aria-hidden="true"></i>{% trans 'Replicate' %}</a></li>
20 20 <li><a href="{% url 'subjects:update' subject.slug %}"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i>{% trans 'Edit' %}</a></li>
  21 + <li><a href="{% url 'groups:index' subject.slug %}"><i class="fa fa-group fa-fw" aria-hidden="true"></i>{% trans 'Groups' %}</a></li>
21 22 <li><a href="javascript:delete_subject.get('{% url 'subjects:delete' subject.slug %}?view=index','#subject','#modal_subject')"><i class="fa fa-trash fa-fw" aria-hidden="true"></i>&nbsp;{% trans 'Remove' %}</a></li>
22 23 </ul>
23 24 {% endif %}
... ...
subjects/templates/subjects/view.html
... ... @@ -9,7 +9,7 @@
9 9  
10 10 {% block breadcrumbs %}
11 11 {{ block.super }}
12   - {% breadcrumb subject.category 'subjects:index' %}
  12 + {% breadcrumb subject.category 'subjects:cat_view' subject.category.slug %}
13 13 {% breadcrumb subject 'subjects:view' subject.slug %}
14 14 {% endblock %}
15 15  
... ... @@ -48,6 +48,7 @@
48 48 <ul class="dropdown-menu pull-right" aria-labelledby="moreActions">
49 49 <li><a href="{% url 'subjects:replicate' subject.slug %}"><i class="fa fa-files-o fa-fw" aria-hidden="true"></i>{% trans 'Replicate' %}</a></li>
50 50 <li><a href="{% url 'subjects:update' subject.slug %}"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i>{% trans 'Edit' %}</a></li>
  51 + <li><a href="{% url 'groups:index' subject.slug %}"><i class="fa fa-group fa-fw" aria-hidden="true"></i>{% trans 'Groups' %}</a></li>
51 52 <li><a href="javascript:delete_subject.get('{% url 'subjects:delete' subject.slug %}?view=index','#subject','#modal_subject')"><i class="fa fa-trash fa-fw" aria-hidden="true"></i>&nbsp;{% trans 'Remove' %}</a></li>
52 53 </ul>
53 54 {% endif %}
... ...
subjects/urls.py
... ... @@ -4,6 +4,7 @@ from . import views
4 4 urlpatterns = [
5 5 url(r'^home/$', views.HomeView.as_view(), name='home'),
6 6 url(r'^$', views.IndexView.as_view(), name='index'),
  7 + url(r'^category/(?P<slug>[\w_-]+)/$', views.IndexView.as_view(), name='cat_view'),
7 8 url(r'^(?P<option>[\w_-]+)/$', views.IndexView.as_view(), name='index'),
8 9 url(r'^create/(?P<slug>[\w_-]+)/$', views.SubjectCreateView.as_view(), name='create'),
9 10 url(r'^replicate/(?P<subject_slug>[\w_-]+)/$', views.SubjectCreateView.as_view(), name='replicate'),
... ...
subjects/utils.py
... ... @@ -18,6 +18,17 @@ def has_professor_profile(user, category):
18 18  
19 19 return False
20 20  
  21 +def get_category_page(categories, slug, per_page):
  22 + total = 1
  23 +
  24 + for category in categories:
  25 + if category.slug == slug:
  26 + return total / per_page + 1
  27 +
  28 + total += 1
  29 +
  30 + return 1
  31 +
21 32 def count_subjects( user, all_subs = True):
22 33 total = 0
23 34 pk = user.pk
... ...
subjects/views.py
... ... @@ -26,7 +26,7 @@ import time
26 26 import datetime
27 27 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
28 28 from .forms import CreateSubjectForm
29   -from .utils import has_student_profile, has_professor_profile, count_subjects
  29 +from .utils import has_student_profile, has_professor_profile, count_subjects, get_category_page
30 30 from users.models import User
31 31  
32 32  
... ... @@ -55,6 +55,7 @@ class HomeView(LoginRequiredMixin, ListView):
55 55 #bringing users
56 56 tags = Tag.objects.all()
57 57 context['tags'] = tags
  58 +
58 59 return context
59 60  
60 61  
... ... @@ -67,8 +68,7 @@ class IndexView(LoginRequiredMixin, ListView):
67 68 context_object_name = 'categories'
68 69 paginate_by = 10
69 70  
70   - def get_queryset(self):
71   -
  71 + def get_queryset(self):
72 72 if self.request.user.is_staff:
73 73 categories = Category.objects.all().order_by('name')
74 74 else:
... ... @@ -94,6 +94,39 @@ class IndexView(LoginRequiredMixin, ListView):
94 94  
95 95 return categories
96 96  
  97 + def paginate_queryset(self, queryset, page_size):
  98 + paginator = self.get_paginator(
  99 + queryset, page_size, orphans=self.get_paginate_orphans(),
  100 + allow_empty_first_page=self.get_allow_empty())
  101 +
  102 + page_kwarg = self.page_kwarg
  103 +
  104 + page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
  105 +
  106 + if self.kwargs.get('slug'):
  107 + categories = queryset
  108 +
  109 + paginator = Paginator(categories, self.paginate_by)
  110 +
  111 + page = get_category_page(categories, self.kwargs.get('slug'), self.paginate_by)
  112 +
  113 + try:
  114 + page_number = int(page)
  115 + except ValueError:
  116 + if page == 'last':
  117 + page_number = paginator.num_pages
  118 + else:
  119 + raise Http404(_("Page is not 'last', nor can it be converted to an int."))
  120 +
  121 + try:
  122 + page = paginator.page(page_number)
  123 + return (paginator, page, page.object_list, page.has_other_pages())
  124 + except InvalidPage as e:
  125 + raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
  126 + 'page_number': page_number,
  127 + 'message': str(e)
  128 + })
  129 +
97 130 def render_to_response(self, context, **response_kwargs):
98 131 if self.request.user.is_staff:
99 132 context['page_template'] = "categories/home_admin_content.html"
... ... @@ -116,11 +149,14 @@ class IndexView(LoginRequiredMixin, ListView):
116 149  
117 150 context['show_buttons'] = True #So it shows subscribe and access buttons
118 151 context['totals'] = self.totals
119   -
  152 +
120 153 if self.kwargs.get('option'):
121 154 context['all'] = True
122 155 context['title'] = _('All Subjects')
123 156  
  157 + if self.kwargs.get('slug'):
  158 + context['cat_slug'] = self.kwargs.get('slug')
  159 +
124 160 context['subjects_menu_active'] = 'subjects_menu_active'
125 161  
126 162 return context
... ...
topics/migrations/0004_auto_20170118_1711.py 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +# -*- coding: utf-8 -*-
  2 +# Generated by Django 1.10 on 2017-01-18 20:11
  3 +from __future__ import unicode_literals
  4 +
  5 +from django.db import migrations
  6 +
  7 +
  8 +class Migration(migrations.Migration):
  9 +
  10 + dependencies = [
  11 + ('topics', '0003_auto_20170116_2101'),
  12 + ]
  13 +
  14 + operations = [
  15 + migrations.AlterModelOptions(
  16 + name='topic',
  17 + options={'ordering': ['order'], 'verbose_name': 'Topic', 'verbose_name_plural': 'Topics'},
  18 + ),
  19 + ]
... ...
topics/templates/topics/create.html
1 1 {% extends 'subjects/view.html' %}
2 2  
3   -{% load django_bootstrap_breadcrumbs %}
  3 +{% load i18n django_bootstrap_breadcrumbs %}
4 4  
5 5 {% block breadcrumbs %}
6 6 {{ block.super }}
  7 +
7 8 {% trans 'Create Topic' as bread %}
8 9 {% breadcrumb bread 'topics:create' subject.slug %}
9 10 {% endblock %}
... ...
topics/templates/topics/update.html
1 1 {% extends 'subjects/view.html' %}
2 2  
3   -{% load django_bootstrap_breadcrumbs %}
  3 +{% load i18n django_bootstrap_breadcrumbs %}
4 4  
5 5 {% block breadcrumbs %}
6 6 {{ block.super }}
  7 +
7 8 {% trans 'Update Topic' as bread %}
8 9 {% breadcrumb bread 'topics:update' subject.slug topic.slug %}
9 10 {% endblock %}
... ...
users/templates/users/search.html
... ... @@ -29,7 +29,7 @@
29 29 </form>
30 30 </div>
31 31 <div class="col-md-3">
32   - <a href="{% url 'users:create' %}" class="pull-right btn btn-primary btn-raised btn-md"><i class="fa fa-plus"></i> {% trans 'Create User' %}</a>
  32 + <a href="{% url 'users:create' %}" class="pull-right btn btn-success btn-raised btn-md"><i class="fa fa-plus"></i> {% trans 'Create User' %}</a>
33 33 </div>
34 34 </div>
35 35  
... ... @@ -53,7 +53,7 @@
53 53 </div>
54 54 <div class="col-md-3">
55 55 <div align="right">
56   - <a href="{% url 'users:update' acc.email %}" class="btn btn-primary btn-raised btn-sm"><i class="fa fa-edit"></i> {% trans 'Edit' %}</a>
  56 + <a href="{% url 'users:update' acc.email %}" class="btn btn-success btn-raised btn-sm"><i class="fa fa-edit"></i> {% trans 'Edit' %}</a>
57 57 <a href="{% url 'users:delete' acc.email %}" class="btn btn-default btn-raised btn-sm"><i class="fa fa-trash"></i> {% trans 'Delete' %}</a>
58 58 </div>
59 59 </div>
... ...
users/views.py
... ... @@ -72,7 +72,7 @@ class SearchView(braces_mixins.LoginRequiredMixin, braces_mixins.StaffuserRequir
72 72 def get_queryset(self):
73 73 search = self.request.GET.get('search', '')
74 74  
75   - users = User.objects.filter(Q(username__icontains = search) | Q(last_name__icontains = search) | Q(social_name__icontains = search) | Q(email__icontains = search)).order_by('social_name','username').exclude(email = self.request.user.email)
  75 + users = User.objects.filter(Q(username__icontains = search) | Q(last_name__icontains = search) | Q(social_name__icontains = search) | Q(email__icontains = search)).distinct().order_by('social_name','username').exclude(email = self.request.user.email)
76 76  
77 77 return users
78 78  
... ...