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,6 +58,7 @@ INSTALLED_APPS = [
58 'log', 58 'log',
59 'categories', 59 'categories',
60 'subjects', 60 'subjects',
  61 + 'students_group',
61 'topics', 62 'topics',
62 'mailsender', 63 'mailsender',
63 'security', 64 'security',
amadeus/static/css/base/amadeus.css
@@ -73,7 +73,7 @@ a:focus { @@ -73,7 +73,7 @@ a:focus {
73 /* side bar menu ends*/ 73 /* side bar menu ends*/
74 74
75 /* category app starts */ 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 padding: 2px 0px; 77 padding: 2px 0px;
78 } 78 }
79 79
amadeus/static/css/themes/green.css
@@ -59,6 +59,19 @@ a, a:focus, a:hover { @@ -59,6 +59,19 @@ a, a:focus, a:hover {
59 background-color: #039BE5 !important; 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 .topic-panel > .panel-heading { 75 .topic-panel > .panel-heading {
63 background-color: #7BA5B9 !important; 76 background-color: #7BA5B9 !important;
64 } 77 }
amadeus/static/js/course.js
@@ -193,4 +193,14 @@ $('.collapse').on('hide.bs.collapse', function (e) { @@ -193,4 +193,14 @@ $('.collapse').on('hide.bs.collapse', function (e) {
193 }); 193 });
194 } 194 }
195 } 195 }
196 -});  
197 \ No newline at end of file 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 \ No newline at end of file 208 \ No newline at end of file
amadeus/urls.py
@@ -27,6 +27,7 @@ urlpatterns = [ @@ -27,6 +27,7 @@ urlpatterns = [
27 url(r'^$', index, name = 'home'), 27 url(r'^$', index, name = 'home'),
28 url(r'^categories/', include('categories.urls', namespace = 'categories')), 28 url(r'^categories/', include('categories.urls', namespace = 'categories')),
29 url(r'^subjects/', include('subjects.urls', namespace = 'subjects')), 29 url(r'^subjects/', include('subjects.urls', namespace = 'subjects')),
  30 + url(r'^groups/', include('students_group.urls', namespace = 'groups')),
30 url(r'^topics/', include('topics.urls', namespace = 'topics')), 31 url(r'^topics/', include('topics.urls', namespace = 'topics')),
31 url(r'^mailsender/', include('mailsender.urls', namespace = 'mailsender')), 32 url(r'^mailsender/', include('mailsender.urls', namespace = 'mailsender')),
32 url(r'^security/', include('security.urls', namespace = 'security')), 33 url(r'^security/', include('security.urls', namespace = 'security')),
categories/migrations/0007_auto_20170118_1711.py 0 → 100644
@@ -0,0 +1,21 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 \ No newline at end of file 44 \ No newline at end of file
students_group/migrations/0001_initial.py 0 → 100644
@@ -0,0 +1,38 @@ @@ -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 @@ @@ -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 @@ @@ -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 \ No newline at end of file 24 \ No newline at end of file
students_group/templates/groups/_form.html 0 → 100644
@@ -0,0 +1,113 @@ @@ -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 \ No newline at end of file 114 \ No newline at end of file
students_group/templates/groups/create.html 0 → 100644
@@ -0,0 +1,30 @@ @@ -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 @@ @@ -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 \ No newline at end of file 24 \ No newline at end of file
students_group/templates/groups/index.html 0 → 100644
@@ -0,0 +1,89 @@ @@ -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 \ No newline at end of file 90 \ No newline at end of file
students_group/templates/groups/update.html 0 → 100644
@@ -0,0 +1,25 @@ @@ -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 @@ @@ -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 @@ @@ -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 \ No newline at end of file 11 \ No newline at end of file
students_group/views.py 0 → 100644
@@ -0,0 +1,174 @@ @@ -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 \ No newline at end of file 175 \ No newline at end of file
subjects/templates/subjects/list.html
@@ -90,6 +90,7 @@ @@ -90,6 +90,7 @@
90 <div id="{{category.slug}}" class="panel-collapse panel-body collapse category-panel-content"> 90 <div id="{{category.slug}}" class="panel-collapse panel-body collapse category-panel-content">
91 <input type="hidden" class="log_url" value="{% url 'categories:view_log' category.id %}" /> 91 <input type="hidden" class="log_url" value="{% url 'categories:view_log' category.id %}" />
92 <input type="hidden" class="log_id" value="" /> 92 <input type="hidden" class="log_id" value="" />
  93 +
93 {% if category.coordinators.all|length > 0 %} 94 {% if category.coordinators.all|length > 0 %}
94 <h4><b>{% trans "Coordinator(s) " %}: </b> 95 <h4><b>{% trans "Coordinator(s) " %}: </b>
95 {{ category.coordinators.all|join:', ' }} 96 {{ category.coordinators.all|join:', ' }}
@@ -193,4 +194,17 @@ @@ -193,4 +194,17 @@
193 <div id="modal_subject"></div> 194 <div id="modal_subject"></div>
194 195
195 <script type="text/javascript" src="{% static 'js/course.js' %}"></script> 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 {% endblock %} 210 {% endblock %}
subjects/templates/subjects/subject_card.html
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 <ul class="dropdown-menu pull-right" aria-labelledby="moreActions"> 18 <ul class="dropdown-menu pull-right" aria-labelledby="moreActions">
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> 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 <li><a href="{% url 'subjects:update' subject.slug %}"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i>{% trans 'Edit' %}</a></li> 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 <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 <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 </ul> 23 </ul>
23 {% endif %} 24 {% endif %}
subjects/templates/subjects/view.html
@@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
9 9
10 {% block breadcrumbs %} 10 {% block breadcrumbs %}
11 {{ block.super }} 11 {{ block.super }}
12 - {% breadcrumb subject.category 'subjects:index' %} 12 + {% breadcrumb subject.category 'subjects:cat_view' subject.category.slug %}
13 {% breadcrumb subject 'subjects:view' subject.slug %} 13 {% breadcrumb subject 'subjects:view' subject.slug %}
14 {% endblock %} 14 {% endblock %}
15 15
@@ -48,6 +48,7 @@ @@ -48,6 +48,7 @@
48 <ul class="dropdown-menu pull-right" aria-labelledby="moreActions"> 48 <ul class="dropdown-menu pull-right" aria-labelledby="moreActions">
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> 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 <li><a href="{% url 'subjects:update' subject.slug %}"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i>{% trans 'Edit' %}</a></li> 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 <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 <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 </ul> 53 </ul>
53 {% endif %} 54 {% endif %}
subjects/urls.py
@@ -4,6 +4,7 @@ from . import views @@ -4,6 +4,7 @@ from . import views
4 urlpatterns = [ 4 urlpatterns = [
5 url(r'^home/$', views.HomeView.as_view(), name='home'), 5 url(r'^home/$', views.HomeView.as_view(), name='home'),
6 url(r'^$', views.IndexView.as_view(), name='index'), 6 url(r'^$', views.IndexView.as_view(), name='index'),
  7 + url(r'^category/(?P<slug>[\w_-]+)/$', views.IndexView.as_view(), name='cat_view'),
7 url(r'^(?P<option>[\w_-]+)/$', views.IndexView.as_view(), name='index'), 8 url(r'^(?P<option>[\w_-]+)/$', views.IndexView.as_view(), name='index'),
8 url(r'^create/(?P<slug>[\w_-]+)/$', views.SubjectCreateView.as_view(), name='create'), 9 url(r'^create/(?P<slug>[\w_-]+)/$', views.SubjectCreateView.as_view(), name='create'),
9 url(r'^replicate/(?P<subject_slug>[\w_-]+)/$', views.SubjectCreateView.as_view(), name='replicate'), 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,6 +18,17 @@ def has_professor_profile(user, category):
18 18
19 return False 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 def count_subjects( user, all_subs = True): 32 def count_subjects( user, all_subs = True):
22 total = 0 33 total = 0
23 pk = user.pk 34 pk = user.pk
subjects/views.py
@@ -26,7 +26,7 @@ import time @@ -26,7 +26,7 @@ import time
26 import datetime 26 import datetime
27 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 27 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
28 from .forms import CreateSubjectForm 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 from users.models import User 30 from users.models import User
31 31
32 32
@@ -55,6 +55,7 @@ class HomeView(LoginRequiredMixin, ListView): @@ -55,6 +55,7 @@ class HomeView(LoginRequiredMixin, ListView):
55 #bringing users 55 #bringing users
56 tags = Tag.objects.all() 56 tags = Tag.objects.all()
57 context['tags'] = tags 57 context['tags'] = tags
  58 +
58 return context 59 return context
59 60
60 61
@@ -67,8 +68,7 @@ class IndexView(LoginRequiredMixin, ListView): @@ -67,8 +68,7 @@ class IndexView(LoginRequiredMixin, ListView):
67 context_object_name = 'categories' 68 context_object_name = 'categories'
68 paginate_by = 10 69 paginate_by = 10
69 70
70 - def get_queryset(self):  
71 - 71 + def get_queryset(self):
72 if self.request.user.is_staff: 72 if self.request.user.is_staff:
73 categories = Category.objects.all().order_by('name') 73 categories = Category.objects.all().order_by('name')
74 else: 74 else:
@@ -94,6 +94,39 @@ class IndexView(LoginRequiredMixin, ListView): @@ -94,6 +94,39 @@ class IndexView(LoginRequiredMixin, ListView):
94 94
95 return categories 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 def render_to_response(self, context, **response_kwargs): 130 def render_to_response(self, context, **response_kwargs):
98 if self.request.user.is_staff: 131 if self.request.user.is_staff:
99 context['page_template'] = "categories/home_admin_content.html" 132 context['page_template'] = "categories/home_admin_content.html"
@@ -116,11 +149,14 @@ class IndexView(LoginRequiredMixin, ListView): @@ -116,11 +149,14 @@ class IndexView(LoginRequiredMixin, ListView):
116 149
117 context['show_buttons'] = True #So it shows subscribe and access buttons 150 context['show_buttons'] = True #So it shows subscribe and access buttons
118 context['totals'] = self.totals 151 context['totals'] = self.totals
119 - 152 +
120 if self.kwargs.get('option'): 153 if self.kwargs.get('option'):
121 context['all'] = True 154 context['all'] = True
122 context['title'] = _('All Subjects') 155 context['title'] = _('All Subjects')
123 156
  157 + if self.kwargs.get('slug'):
  158 + context['cat_slug'] = self.kwargs.get('slug')
  159 +
124 context['subjects_menu_active'] = 'subjects_menu_active' 160 context['subjects_menu_active'] = 'subjects_menu_active'
125 161
126 return context 162 return context
topics/migrations/0004_auto_20170118_1711.py 0 → 100644
@@ -0,0 +1,19 @@ @@ -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 {% extends 'subjects/view.html' %} 1 {% extends 'subjects/view.html' %}
2 2
3 -{% load django_bootstrap_breadcrumbs %} 3 +{% load i18n django_bootstrap_breadcrumbs %}
4 4
5 {% block breadcrumbs %} 5 {% block breadcrumbs %}
6 {{ block.super }} 6 {{ block.super }}
  7 +
7 {% trans 'Create Topic' as bread %} 8 {% trans 'Create Topic' as bread %}
8 {% breadcrumb bread 'topics:create' subject.slug %} 9 {% breadcrumb bread 'topics:create' subject.slug %}
9 {% endblock %} 10 {% endblock %}
topics/templates/topics/update.html
1 {% extends 'subjects/view.html' %} 1 {% extends 'subjects/view.html' %}
2 2
3 -{% load django_bootstrap_breadcrumbs %} 3 +{% load i18n django_bootstrap_breadcrumbs %}
4 4
5 {% block breadcrumbs %} 5 {% block breadcrumbs %}
6 {{ block.super }} 6 {{ block.super }}
  7 +
7 {% trans 'Update Topic' as bread %} 8 {% trans 'Update Topic' as bread %}
8 {% breadcrumb bread 'topics:update' subject.slug topic.slug %} 9 {% breadcrumb bread 'topics:update' subject.slug topic.slug %}
9 {% endblock %} 10 {% endblock %}
users/templates/users/search.html
@@ -29,7 +29,7 @@ @@ -29,7 +29,7 @@
29 </form> 29 </form>
30 </div> 30 </div>
31 <div class="col-md-3"> 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 </div> 33 </div>
34 </div> 34 </div>
35 35
@@ -53,7 +53,7 @@ @@ -53,7 +53,7 @@
53 </div> 53 </div>
54 <div class="col-md-3"> 54 <div class="col-md-3">
55 <div align="right"> 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 <a href="{% url 'users:delete' acc.email %}" class="btn btn-default btn-raised btn-sm"><i class="fa fa-trash"></i> {% trans 'Delete' %}</a> 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 </div> 58 </div>
59 </div> 59 </div>
users/views.py
@@ -72,7 +72,7 @@ class SearchView(braces_mixins.LoginRequiredMixin, braces_mixins.StaffuserRequir @@ -72,7 +72,7 @@ class SearchView(braces_mixins.LoginRequiredMixin, braces_mixins.StaffuserRequir
72 def get_queryset(self): 72 def get_queryset(self):
73 search = self.request.GET.get('search', '') 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 return users 77 return users
78 78