...
 
Commits (59)
This diff is collapsed.
......@@ -9,7 +9,7 @@ from .models import (
Candidate, JuryMember, JuryMemberCompetition, Evaluation, EvaluationField, EvaluationNote,
ApplicationFormFieldType, CompetitionAttribute, DynamicApplicationForm, ApplicationFormFieldSetDefinition,
ApplicationFormFieldDefinition, CandidateGroup, JuryMemberGroup, CompetitionAttributeValue, ApplicationDraft,
EvaluationType, Call, StaticContent, EvaluationStatus
EvaluationType, Call, StaticContent, EvaluationStatus, TemporaryDocumentExtraFile
)
from ulysses.composers.factories import ComposerFactory, UserFactory
from ulysses.partners.factories import PartnerFactory
......@@ -197,6 +197,11 @@ class TemporaryDocumentFactory(factory.DjangoModelFactory):
model = TemporaryDocument
class TemporaryDocumentExtraFileFactory(factory.DjangoModelFactory):
class Meta:
model = TemporaryDocumentExtraFile
class TemporaryMediaFactory(factory.DjangoModelFactory):
class Meta:
model = TemporaryMedia
......
# -*- coding: utf-8 -*-
import re
from django.conf import settings
from django.urls import reverse
......@@ -95,13 +93,13 @@ class DocumentField(ApplicationField):
def get_value_key(self):
return "%s_hidden" % self.name
def __init__(self, competition, name, label, required=True, help_text=""):
def __init__(self, competition, name, label, extra_files=0, required=True, help_text=""):
super(DocumentField, self).__init__(
competition, name, label, required, help_text,
DocumentEditWidget(competition=competition),
DocumentPreviewWidget(competition=competition),
DocumentAdminWidget(),
DocumentPreviewWidget(competition=competition),
DocumentEditWidget(competition=competition, extra_files=extra_files),
DocumentPreviewWidget(competition=competition, extra_files=extra_files),
DocumentAdminWidget(extra_files=extra_files),
DocumentPreviewWidget(competition=competition, extra_files=extra_files),
)
def get_value_for_candidate(self, candidate, text=True):
......@@ -123,9 +121,7 @@ class DocumentField(ApplicationField):
return ''
def as_html(self, document):
return '<a href="{0}{1}" target="_blank">{2}</a>'.format(
settings.MEDIA_URL, document.file, document.title
)
return document.as_html
class Meta:
model = TemporaryDocument
......
......@@ -129,12 +129,14 @@ class CandidateNotificationForm(forms.Form):
from_email = forms.EmailField(label=_("From email"), widget=forms.TextInput(attrs={'size': '80'}))
subject = forms.CharField(label=_("Subject"), widget=forms.TextInput(attrs={'size': '80'}))
body = forms.CharField(widget=forms.Textarea(attrs={'cols': 80, 'rows': 20}), help_text=HELP_TEXT)
cc_me = forms.BooleanField(label="I want to be cced of all notifications sent", required=False)
class JuryMemberNotificationForm(forms.Form):
from_email = forms.EmailField(label=_("From email"), widget=forms.TextInput(attrs={'size': '80'}))
subject = forms.CharField(label=_("Subject"), widget=forms.TextInput(attrs={'size': '80'}))
body = forms.CharField(widget=forms.Textarea(attrs={'cols': 80}))
cc_me = forms.BooleanField(label="I want to be cced of all notifications sent", required=False)
class JuryMemberEditForm(forms.Form):
......@@ -152,8 +154,8 @@ def get_candidate_document_key_validator(candidate):
return _clean_key
def get_candidate_document_modelform(candidate, edit_mode=False):
def get_candidate_document_modelform(candidate, field_definition):
extra_files = []
class _CandidateDocumentForm(forms.ModelForm):
file = forms.CharField(
label="file",
......@@ -186,6 +188,26 @@ def get_candidate_document_modelform(candidate, edit_mode=False):
self.instance = instance
# If No value -> Delete the document
self.fields['file'].required = False
self.fields['file'].help_text += ' If empty, the document will be deleted'
self.extra_files = list(instance.get_extra_files()) if instance else []
for index in range(field_definition.extra_files):
field_name = 'extra_file_{0}'.format(index)
initial = ''
if index < len(self.extra_files):
initial = self.get_name(self.extra_files[index].file)
self.fields[field_name] = forms.CharField(
label="additional file {0}".format(index + 1),
help_text=_('Supported formats: pdf, jpg. 100MB max.'),
initial=initial,
required=False,
widget=FileUploadControl(
upload_url=reverse_lazy('upload_from_superadmin', args=[candidate.id]),
allowed_extensions='pdf|jpg',
no_help=True
)
)
def clean_file(self):
is_new_file = self._data.get('file_path', '')
......@@ -200,6 +222,23 @@ def get_candidate_document_modelform(candidate, edit_mode=False):
# The file has been cleared
return ''
def clean(self):
for index in range(field_definition.extra_files):
field_name = 'extra_file_{0}'.format(index)
field_name_path = 'extra_file_{0}_path'.format(index)
is_new_file = self._data.get(field_name_path, '')
extra_file = self.extra_files[index] if index < len(self.extra_files) else None
if is_new_file:
self.cleaned_data[field_name] = os.path.join(self.get_target(), self.cleaned_data[field_name])
else:
value = self.cleaned_data[field_name]
if value:
# No change was made (no new file uploaded)
self.cleaned_data[field_name] = extra_file.file if extra_file else ''
else:
# The file has been cleared
self.cleaned_data[field_name] = ''
return _CandidateDocumentForm
......
# Generated by Django 2.2.9 on 2020-02-12 10:28
from django.db import migrations
def forward(apps, schema_editor):
"""set names of static contents"""
# Force the permission
call_request_class = apps.get_model("ulysses_competitions", "CallRequest")
for call_request in call_request_class.objects.all():
if call_request.picture and '/None/' in call_request.picture.name:
call_request.picture.name = call_request.picture.name.replace('/None/', '/0/')
call_request.save()
class Migration(migrations.Migration):
dependencies = [
('ulysses_competitions', '0049_auto_20200207_1534'),
]
operations = [
migrations.RunPython(forward, migrations.RunPython.noop)
]
# Generated by Django 2.2.9 on 2020-03-26 12:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def forward(apps, schema_editor):
competition_manager_model = apps.get_model("ulysses_competitions", "CompetitionManager")
competition_model = apps.get_model("ulysses_competitions", "Competition")
all_clones = []
for competition_manager in competition_manager_model.objects.all():
if competition_manager.id not in all_clones:
clones = competition_manager_model.objects.filter(
user=competition_manager.user
).exclude(id=competition_manager.id)
if clones.count():
for clone in clones:
print('\nclone for', clone.user, 'reassign', clone, 'to', competition_manager)
for competition in competition_model.objects.filter(managed_by=clone):
competition.managed_by.add(competition_manager)
competition.save()
all_clones.append(clone.id)
if competition_manager_model.objects.filter(id__in=all_clones).exists():
print("delete", competition_manager_model.objects.filter(id__in=all_clones).delete())
class Migration(migrations.Migration):
dependencies = [
('ulysses_competitions', '0050_auto_20200212_1028'),
]
operations = [
migrations.RunPython(forward, migrations.RunPython.noop),
migrations.AlterField(
model_name='competitionmanager',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]
# Generated by Django 2.2.9 on 2020-04-01 10:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ulysses_competitions', '0051_auto_20200326_1221'),
]
operations = [
migrations.AddField(
model_name='competition',
name='entry_fee',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='Entry or course fee'),
),
migrations.AddField(
model_name='competition',
name='restrictions',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='Restrictions'),
),
]
# Generated by Django 2.2.9 on 2020-04-01 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ulysses_competitions', '0052_auto_20200401_1052'),
]
operations = [
migrations.AlterField(
model_name='competition',
name='entry_fee',
field=models.CharField(blank=True, default='', help_text='Max 200 characters', max_length=200, verbose_name='Entry or course fee'),
),
migrations.AlterField(
model_name='competition',
name='restrictions',
field=models.CharField(blank=True, default='', help_text='Max 200 characters', max_length=200, verbose_name='Restrictions'),
),
]
# Generated by Django 2.2.9 on 2020-04-01 15:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ulysses_competitions', '0053_auto_20200401_1221'),
]
operations = [
migrations.AddField(
model_name='applicationformfielddefinition',
name='extra_files',
field=models.IntegerField(default=0, help_text='Documents only. Number of allowed addition files'),
),
migrations.CreateModel(
name='TemporaryDocumentExtraFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(help_text="file url, relative to application's MEDIA_URL", max_length=400, verbose_name='file')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_files', to='ulysses_competitions.TemporaryDocument')),
],
options={
'verbose_name': 'document extra file',
'abstract': False,
},
),
migrations.CreateModel(
name='DraftDocumentExtraFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(help_text="file url, relative to application's MEDIA_URL", max_length=400, verbose_name='file')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_files', to='ulysses_competitions.DraftDocument')),
],
options={
'verbose_name': 'document extra file',
'abstract': False,
},
),
migrations.CreateModel(
name='CandidateDocumentExtraFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(help_text="file url, relative to application's MEDIA_URL", max_length=400, verbose_name='file')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_files', to='ulysses_competitions.CandidateDocument')),
],
options={
'verbose_name': 'document extra file',
'abstract': False,
},
),
]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -89,6 +89,7 @@ class OrganizeCallTest(BaseTestCase):
self.assertEqual(date.today(), call.request_datetime.date())
self.assertEqual(False, call.processed)
self.assertNotEqual(call.picture, None)
self.assertEqual(call.picture.name, 'calls/0/test.png')
def test_post_organize_call_logged(self):
"""it should redirect to login page"""
......@@ -119,6 +120,7 @@ class OrganizeCallTest(BaseTestCase):
self.assertEqual(date.today(), call.request_datetime.date())
self.assertEqual(False, call.processed)
self.assertNotEqual(call.picture, None)
self.assertEqual(call.picture.name, 'calls/1/test.png')
def test_post_empty_name(self):
"""it should send message to admin"""
......
......@@ -227,10 +227,11 @@ class CompetitionCreationTest(TestCase):
self.assertTrue(logged_in)
# Select competition to administrate
select_competition_url = "/admin/select_competition/%s" % competition.pk
select_competition_url = reverse('competition-admin:select_competition', args=[competition.pk])
response = self.client.get(select_competition_url)
self.assertEqual(response.status_code, 302)
competition_admin_url = '/admin/infos/'
competition_admin_url = reverse('competition-admin:show_informations')
competition_admin_response = self.client.get(competition_admin_url)
self.assertContains(competition_admin_response, title)
self.assertContains(competition_admin_response, "Informations")
......
......@@ -10,7 +10,7 @@ import floppyforms.__future__ as forms
from ulysses.competitions.models import CompetitionAttributeValue
from ulysses.generic.widgets import DEFAULT_TINCY_MCE_ATTRS, MINI_TINCY_MCE_ATTRS
from ulysses.generic.utils import url_to_link
from ulysses.generic.utils import url_to_link, get_request
from .models import TemporaryDocument, TemporaryMedia
......@@ -235,8 +235,10 @@ class BiographicElementAdminWidget(ElementAdmindWidget):
class DocumentAdminWidget(ElementWithHiddenFieldWidget):
extra_files = False
def __init__(self, attrs=None):
def __init__(self, extra_files, attrs=None):
self.extra_files = extra_files
default_attrs = {'class': 'CandidateDocument'}
if attrs:
default_attrs.update(attrs)
......@@ -310,11 +312,16 @@ class BiographicElementEditWidget(forms.Textarea):
class DocumentBaseWidget(ElementWithHiddenFieldWidget):
extra_files = False
preview = False
def get_value_for_display(self, value):
if value:
try:
return TemporaryDocument.objects.get(id=value).as_html
if self.extra_files:
return TemporaryDocument.objects.get(id=value).as_edit_html(self.preview)
else:
return TemporaryDocument.objects.get(id=value).as_html
except (ValueError, TemporaryDocument.DoesNotExist):
pass
return value
......@@ -325,24 +332,39 @@ class DocumentBaseWidget(ElementWithHiddenFieldWidget):
class DocumentEditWidget(DocumentBaseWidget):
def __init__(self, competition, *args, **kwargs):
def __init__(self, competition, extra_files, *args, **kwargs):
self.competition = competition
self.extra_files = extra_files
super(DocumentEditWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, **kwargs):
html = super(DocumentEditWidget, self).render(name, value, attrs, **kwargs)
button1_html = '''
<a class="btn-icon colorbox-form" href="{0}"><img src="{1}icons/upload-doc2.svg" /></a>
'''.format(
reverse('web:upload_document', args=[self.competition.url, name]),
settings.STATIC_URL
)
button2_html = '''
<div href class="btn-icon application-remove-document"><img src="{0}icons/trash-can-green.svg" /></div>
'''.format(
settings.STATIC_URL
)
if allow_import_from_personal_space():
if not self.extra_files:
button2_html = '''
<div href class="btn-icon application-remove-document"><img src="{0}icons/trash-can-green.svg" /></div>
'''.format(
settings.STATIC_URL
)
else:
button2_html = '''
<a href="{1}" id="{3}_add_file" class="{4} btn-icon colorbox-form"><img src="{0}icons/add-square.svg" /></a>
<a href="{2}" class="btn-icon colorbox-form"><img src="{0}icons/trash-can-green.svg" /></a>
'''.format(
settings.STATIC_URL,
reverse('web:add_temporary_document_file', args=[self.competition.url, name]),
reverse('web:remove_document', args=[self.competition.url, name]),
name,
'disabled' if not value else ''
)
if not self.extra_files and allow_import_from_personal_space():
button3_html = '<a href={0} class="import-portfolio colorbox-form">{1}</a>'.format(
reverse('web:select_document_from_portfolio', args=[self.competition.url, name]),
_('Upload from profile'),
......@@ -358,8 +380,10 @@ class DocumentEditWidget(DocumentBaseWidget):
class DocumentPreviewWidget(DocumentBaseWidget):
def __init__(self, competition, *args, **kwargs):
def __init__(self, competition, extra_files, *args, **kwargs):
self.competition = competition
self.extra_files = extra_files
self.preview = True
super(DocumentPreviewWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, **kwargs):
......
......@@ -4,7 +4,7 @@ from django.conf import settings
from django.contrib.admin import ModelAdmin, TabularInline
from ulysses.composers.models import Composer, Gender
from ulysses.composers.models import Media, Document, BiographicElement
from ulysses.composers.models import Media, Document, BiographicElement, DocumentExtraFile
from ulysses.super_admin.sites import super_admin_site
......@@ -70,11 +70,18 @@ class MediaAdmin(ModelAdmin):
search_fields = ['id', 'composer__user__username', 'composer__user__last_name', 'title']
class DocumentExtraFileInline(TabularInline):
model = DocumentExtraFile
fields = ['file', ]
extra = 0
class DocumentAdmin(ModelAdmin):
list_display = ('composer', 'id', 'title', 'creation_date', 'update_date')
list_display_links = ('id', 'title',)
list_filter = ['composer',]
list_display_links = ('id', 'title', )
raw_id_fields = ['composer', ]
search_fields = ['id', 'composer__user__username', 'composer__user__last_name', 'title']
inlines = [DocumentExtraFileInline, ]
class BiographicElementAdmin(ModelAdmin):
......
# Generated by Django 2.2.9 on 2020-04-01 15:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ulysses_composers', '0013_auto_20200207_1534'),
]
operations = [
migrations.CreateModel(
name='DocumentExtraFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(help_text="file url, relative to application's MEDIA_URL", max_length=400, verbose_name='file')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_files', to='ulysses_composers.Document')),
],
options={
'verbose_name': 'document extra file',
'abstract': False,
},
),
]
......@@ -5,6 +5,7 @@ import sys
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse
from django.db import models
from django.utils.translation import ugettext as _
......@@ -277,7 +278,14 @@ class DocumentBase(models.Model):
return result
def get_html(self):
return '<a href="{0}{1}" target="_blank">{2}</a>'.format(settings.MEDIA_URL, self.file, self.title)
html = ''
if self.file:
html = '{2}: <a class="document-file-item" href="{0}{1}" target="_blank">{3}</a>'.format(
settings.MEDIA_URL, self.file, self.title, os.path.split(self.file)[-1]
)
for extra_file in self.get_extra_files():
html += extra_file.as_edit_html()
return html
def get_filename(self):
return os.path.basename(self.file)
......@@ -291,13 +299,16 @@ class DocumentBase(models.Model):
Nota : the physical file itself if copied
"""
from ulysses.competitions.models import create_temporary_document
return create_temporary_document(competition, composer, key, self.title, self.file)
value = create_temporary_document(competition, composer, key, self.title, self.file, self.get_extra_files())
return value
def move_to_other_composer(self, composer):
# Create new candidate media
from ulysses.competitions.models import move_file_to_other_personal_folder
self.file = move_file_to_other_personal_folder(composer, self.file)
self.save()
for extra_file in self.get_extra_files():
extra_file.move_to_other_composer(composer)
def reassign_to_other_composer(self, composer, old_username):
# Create new candidate media
......@@ -308,11 +319,67 @@ class DocumentBase(models.Model):
print('> move file', self.file, 'to personal folder', composer.user.username)
self.file = move_file_to_other_personal_folder(composer, self.file)
self.save()
for extra_file in self.get_extra_files():
extra_file.reassign_to_other_composer(composer, old_username)
def get_extra_files(self):
raise NotImplemented
@property
def as_html(self):
return self.as_edit_html(True)
def as_edit_html(self, preview=False):
html = '{2}: <a class="document-file-item" href="{0}{1}" target="_blank">{3}</a>'.format(
settings.MEDIA_URL, self.file, self.title, os.path.split(self.file)[-1]
)
for extra_file in self.get_extra_files():
html += extra_file.as_edit_html(preview)
return html
class Meta:
abstract = True
verbose_name = _("document")
ordering = ['creation_date',]
ordering = ['creation_date', ]
class DocumentExtraFileBase(models.Model):
file = models.CharField(
verbose_name=_("file"), max_length=400, help_text="file url, relative to application's MEDIA_URL"
)
class Meta:
abstract = True
verbose_name = _("document extra file")
def move_to_other_composer(self, composer):
# Create new candidate media
from ulysses.competitions.models import move_file_to_other_personal_folder
self.file = move_file_to_other_personal_folder(composer, self.file)
self.save()
def reassign_to_other_composer(self, composer, old_username):
# Create new candidate media
from ulysses.competitions.models import move_file_to_other_personal_folder
if old_username in self.file:
verbose = 'reassign_candidate_files' in sys.argv
if verbose:
print('> move file', self.file, 'to personal folder', composer.user.username)
self.file = move_file_to_other_personal_folder(composer, self.file)
self.save()
@property
def as_html(self):
html = '<span class="document-file-item"><a href="{0}{1}" target="_blank">{2}</a></span>'.format(
settings.MEDIA_URL, self.file, os.path.split(self.file)[-1]
)
return html
def as_edit_html(self, preview=False):
html = '<span class="document-file-item"><a href="{0}{1}" target="_blank">{2}</a></span>'.format(
settings.MEDIA_URL, self.file, os.path.split(self.file)[-1]
)
return html
class Document(DocumentBase):
......@@ -323,6 +390,13 @@ class Document(DocumentBase):
from ulysses.competitions.models import delete_file_helper
delete_file_helper(self.file)
def get_extra_files(self):
return self.extra_files.all()
class DocumentExtraFile(DocumentExtraFileBase):
document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name='extra_files')
class BiographicElementBase(models.Model):
"""
......
......@@ -21,7 +21,7 @@ class EventAdmin(ModelAdmin):
)
date_hierarchy = 'start_date'
list_filter = ("ulysses_label", 'event_type', 'country', 'visibility', )
raw_id_fields = ('organizators', 'performers', 'works', )
raw_id_fields = ('organizators', 'performers', 'works', 'owner', 'members_involved', )
inlines = [EventCollaboratorInline]
search_fields = ('title', )
......
......@@ -43,22 +43,20 @@ def get_forthcoming_events_queryset():
return queryset
def get_upcoming_events_queryset():
def get_upcoming_events_queryset(take_time_into_account=False):
"""
Returns: queryset of events in future
"""
today_datetime = datetime.combine(date.today(), time.min)
if take_time_into_account:
today_datetime = datetime.now()
else:
today_datetime = datetime.combine(date.today(), time.min)
queryset = Event.objects.filter(
Q(start_date__gte=today_datetime, end_date__isnull=True) |
Q(end_date__gte=today_datetime, end_date__isnull=False)
).order_by('start_date')
# # DEBUG
# if queryset.count() == 0:
# queryset = Event.objects.all().order_by('-start_date')
return queryset
......
......@@ -29,7 +29,7 @@ from ulysses.social.models import FeedItem, MemberPost
from .forms import EventForm, EventSearchForm, SaveEventSearchForm
from .models import Event, UserEventAlert
from .utils import get_upcoming_events_queryset, get_events_in_range, filter_events
from .utils import get_upcoming_events_queryset, filter_events
@csrf_exempt
......
......@@ -35,7 +35,7 @@ class DirectoryName:
user_id = instance.owner.id
else:
request = get_request()
user_id = request.user.id if request and request.user else '0'
user_id = request.user.id if (request and request.user and request.user.id) else '0'
user_directory = get_user_directory(self.parent_directory, user_id)
return os.path.join(user_directory, file_name)
......
# -*- coding: utf-8 -*-
from django.conf import settings
from django.core.mail import get_connection, EmailMultiAlternatives
from django.core.mail import get_connection, EmailMultiAlternatives, EmailMessage
from django.template.loader import get_template
from ulysses.utils import dehtml
......@@ -21,3 +21,15 @@ def send_email(subject, template_name, context, dests, sender=None, cc_list=None
email.attach_alternative(html_text, "text/html")
emails.append(email)
return connection.send_messages(emails)
def send_text_email(subject, msg_body, from_email, tos, ccs=None):
"""Send text email"""
email = EmailMessage(
subject,
msg_body,
from_email if from_email else settings.DEFAULT_FROM_EMAIL,
tos,
cc=ccs if ccs is not None else [],
)
email.send()
......@@ -3,13 +3,16 @@
import collections
import os.path
from django.conf import settings
from django.forms.forms import BoundField
from django.forms.widgets import SelectMultiple
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
import floppyforms.__future__ as forms
from ulysses.utils import is_mp3_valid
from .widgets import FileUploadControl
......@@ -108,7 +111,6 @@ class FileUploadForm(BaseModelForm):
fields_order = []
remove_fields = []
media_field_names = self.get_upload_fields()
if media_field_names:
# Transform file fields to upload fields
for field_name in list(self.fields):
......@@ -137,6 +139,7 @@ class FileUploadForm(BaseModelForm):
label = model_field.verbose_name
except AttributeError:
pass
# TODO for MP3 check
media_field = forms.CharField(
label=label,
required=required,
......@@ -148,6 +151,20 @@ class FileUploadForm(BaseModelForm):
max_length=200,
help_text=help_text
)
if 'mp3' in allowed_extensions:
_media_field_clean = media_field.clean
patched_field_name = field_name
def patched_clean(value):
if value:
if os.path.splitext(value)[1].lower() == '.mp3':
full_value = self.get_relative_file_path(patched_field_name, value)
full_value = os.path.join(settings.MEDIA_ROOT, full_value)
if not is_mp3_valid(full_value):
raise forms.ValidationError(_('It seems that this file is not a valid mp3'))
return _media_field_clean(value)
media_field.clean = patched_clean
# Try to load initial value
instance_value = getattr(instance, field_name, None)
......
......@@ -806,6 +806,16 @@ a.guidelines img {
font-weight: normal;
}
.section-media-item .section-media-actions {
margin-right: 10px;
}
.section-media-item .section-media-actions:last-of-type {
margin-right: 0;
}
.not-jury-warning {
text-align: center;
padding: 10px;
......
......@@ -204,8 +204,19 @@
<div>Document</div> <div class="section-item-dots"></div>
</div>
<div class="section-media-actions">
<a href="{{ MEDIA_URL }}{{ media_element.doc.file }}" target="_blank">Show</a>
<a href="{{ MEDIA_URL }}{{ media_element.doc.file }}" target="_blank">
Show {% if media_element.doc.get_extra_files.count %}1{% endif %}
</a>
</div>
{% if media_element.doc.get_extra_files.count %}
{% for extra_file in media_element.doc.get_extra_files %}
<div class="section-media-actions">
<a href="{{ MEDIA_URL }}{{ extra_file.file }}" target="_blank">
Show {{ forloop.counter|add:1 }}
</a>
</div>
{% endfor %}
{% endif %}
</div></li>
</ul>
{% endif %}
......
{% load i18n %}{% trans "Hello" %}
{% trans "A completed evaluation has been modified by a jury member" %}
{% trans "Competition:" %} {{ competition.name }}
{% trans "Competition step:" %} {{ competition_step.name }}
{% trans "Jury member:" %} {{ jury_member.name }}
{% trans "Candidate:" %} {{ candidate.full_name }}
......@@ -6,13 +6,14 @@ from bs4 import BeautifulSoup
import os.path
from os.path import abspath, dirname
from django.core import mail
from django.http import HttpResponseRedirect
from django.test.client import RequestFactory
from django.urls import reverse
from ulysses.competitions.models import (
CompetitionStatus, is_jury_member, Evaluation, EvaluationNote, CandidateAttributeValue,
ApplicationFormFieldType, CandidateBiographicElement, ApplicationElement
ApplicationFormFieldType, CandidateBiographicElement, ApplicationElement, EvaluationStatus
)
from ulysses.competitions.factories import (
JuryMemberFactory, CompetitionFactory, CompetitionStepFactory, CandidateFactory, EvaluationFactory,
......@@ -265,6 +266,63 @@ class JuryInterfaceTest(JuryInterfaceTestBaseTest):
note = EvaluationNote.objects.get(evaluation=evaluation, key=self.evaluation_field.key)
self.assertEqual(note.value, str(note_value))
def test_evaluate_candidate_modify(self):
candidate_url = "/jury/dashboard/?step=%s&status=all&candidate=%s" % (
self.competition_step.pk, self.candidate.pk
)
evaluation = Evaluation.objects.get(
candidate=self.candidate,
jury_member=self.jury_member,
competition_step=self.competition_step
)
EvaluationNoteFactory.create(
evaluation=evaluation, key=self.evaluation_field.key, value=2
)
evaluation.status = EvaluationStatus.objects.get(url='completed')
evaluation.save()
resp = self.client.get(candidate_url)
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, self.candidate.composer.user.first_name)
self.assertContains(resp, self.candidate.composer.user.last_name)
note_value = 3
post_data = {
'candidate_id': self.candidate.pk,
'note': note_value,
'is_completed': True
}
resp = self.client.post(candidate_url, post_data)
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, self.candidate.composer.user.first_name)
self.assertContains(resp, self.candidate.composer.user.last_name)
self.assertContains(resp, "Your changes have been saved")
# Ensure evaluation has been updated
evaluation = Evaluation.objects.get(
candidate=self.candidate, jury_member=self.jury_member,
competition_step=self.competition_step
)
self.assertEqual(evaluation.status.url, "completed")
note = EvaluationNote.objects.get(evaluation=evaluation, key=self.evaluation_field.key)
self.assertEqual(note.value, str(note_value))
# An email has been sent to admins
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [self.manager.user.email])
body = mail.outbox[0].body
self.assertTrue(body.find(self.competition.name) >= 0)
self.assertTrue(body.find(self.competition_step.name) >= 0)
self.assertTrue(body.find(self.candidate.full_name()) >= 0)
self.assertTrue(body.find(self.jury_member.name()) >= 0)
class JuryInterfaceCommentsTest(JuryInterfaceTestBaseTest):
......
# -*- coding: utf-8 -*-
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.shortcuts import render, redirect, get_object_or_404
......@@ -14,6 +15,7 @@ from ulysses.competitions.models import (
JuryApplicationElementToExclude, CompetitionManager, CandidateDocument
)
from ulysses.competitions.utils import is_competition_admin
from ulysses.generic.emails import send_text_email
from ulysses.jury.forms import JuryProfileForm, CandidateFilterForm
from ulysses.utils import json_response, JsonResponse
......@@ -270,6 +272,7 @@ def show_dashboard(request):
else:
evaluation.set_note(key, val)
# Set evaluation status
was_completed = evaluation.status and evaluation.status.url == 'completed'
if evaluation_form.cleaned_data["is_completed"]:
evaluation.status = EvaluationStatus.objects.get(url="completed")
else:
......@@ -280,6 +283,27 @@ def show_dashboard(request):
evaluation.recommendation = evaluation_form.cleaned_data["recommendation"]
# Save changes
evaluation.save()
if was_completed:
admins_emails = [
manager.user.email
for manager in evaluation.competition_step.competition.managed_by.all()
]
context = {
'evaluation': evaluation,
'competition': evaluation.competition_step.competition,
'competition_step': evaluation.competition_step,
'candidate': evaluation.candidate,
'jury_member': evaluation.jury_member,
}
the_template = loader.get_template('jury/evaluation_changed_notification.txt')
body = the_template.render(context)
send_text_email(
_('A jury member has changed their evaluation'),
body,
settings.DEFAULT_FROM_EMAIL,
admins_emails
)
params["message"] = _("Your changes have been saved")
else:
params["error_message"] = _("Please correct the errors below")
......
......@@ -562,6 +562,32 @@ class RegistrationTest(BaseRegistrationTest):
self.assertEqual(Individual.objects.all()[0].user, user)
self.assertEqual(Organization.objects.count(), 0)
def test_register_no_user_type(self):
"""it should create user even if email is longer than 3O chars (limit for username)"""
url = reverse('registration_register')
password = 'Poseidon-1234'
post_data = {
# 'user_type': 'individual',
'last_name': 'Dalton',
'first_name': 'Joe',
'username': '',
'email': 'joe@dalton.com',
'password1': password,
'password2': password,
'recaptcha_response_field': 'PASSED'
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 200)
soup = BeautifulSoup(response.content, 'html.parser')
self.assertEqual(1, len(soup.select(".field-error")))
self.assertEqual(0, User.objects.count())
self.assertEqual(mail.outbox, [])
self.assertEqual(Individual.objects.count(), 0)
self.assertEqual(Organization.objects.count(), 0)
def test_register_no_captcha(self):
"""it should create user for an individual"""
del os.environ['RECAPTCHA_DISABLE']
......
......@@ -185,7 +185,7 @@ class MemberPostForm(FileUploadForm):
return '<iframe width="100%" height="450" scrolling="no" frameborder="no" src="{0}"></iframe>'.format(url)
def is_youtube(self, url):
if re.match('https://www.youtube.com/.*', url):
if re.match('https://www.youtube.com/.*', url) or re.match('https://youtu.be/.*', url):
return '<iframe width="100%" height="450" scrolling="no" frameborder="0" src="{0}"></iframe>'.format(url)
def clean_iframe(self):
......
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, time
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
......@@ -141,7 +141,7 @@ class CommunityNewsTest(BaseTestCase):
user = individual.user
self.assertTrue(self.client.login(email=user.email, password="1234"))
future_date = datetime.now() + timedelta(days=1)
today_date = datetime.now()
today_date = datetime.combine(date.today(), time.max)
past_date = datetime.now() - timedelta(days=1)
event1 = EventFactory.create(start_date=future_date)
event2 = EventFactory.create(start_date=today_date)
......@@ -154,34 +154,74 @@ class CommunityNewsTest(BaseTestCase):
soup = BeautifulSoup(response.content, 'html.parser')
self.assertEqual(len(soup.select(".upcoming-events")), 1)
self.assertEqual(len(soup.select(".upcoming-events .event-summary")), 3)
self.assertContains(response, 'Upcoming events')
self.assertContains(response, event1.title)
self.assertContains(response, event2.title)
self.assertNotContains(response, event3.title)
self.assertNotContains(response, event4.title)
self.assertContains(response, event5.title)
def test_view_works(self):
def test_view_today_events(self):
# Create a active user
individual = IndividualFactory.create()
user = individual.user
self.assertTrue(self.client.login(email=user.email, password="1234"))
work1 = WorkFactory.create()
FeedItemFactory.create(content_object=work1)
future_date = datetime.now() + timedelta(days=1)
event1 = EventFactory.create(start_date=future_date) # create a future event for the upcoming events
event2 = EventFactory.create(start_date=None) # shared -> should be in timeline and not in the upcoming events
FeedItemFactory.create(content_object=event2)
other_post = MemberPostFactory.create()
later_today = datetime.now() + timedelta(hours=1)
earlier_today = datetime.now() - timedelta(hours=1)
earlier_today2 = datetime.now() - timedelta(hours=2)
event1 = EventFactory.create(start_date=later_today)
event2 = EventFactory.create(start_date=earlier_today)
event3 = EventFactory.create(start_date=earlier_today, end_date=later_today)
event4 = EventFactory.create(start_date=earlier_today2, end_date=earlier_today)
url = reverse('social:community_news')
response = self.client.get(url)
self.assertEqual(200, response.status_code)
soup = BeautifulSoup(response.content, 'html.parser')
self.assertEqual(len(soup.select(".upcoming-events")), 1)
self.assertEqual(len(soup.select(".upcoming-events .event-summary")), 2)
self.assertContains(response, 'Upcoming events')
self.assertContains(response, event1.title)
self.assertContains(response, event3.title)
self.assertNotContains(response, event2.title)
self.assertNotContains(response, event4.title)
def test_view_finish_today_events(self):
# Create a active user
individual = IndividualFactory.create()
user = individual.user
self.assertTrue(self.client.login(email=user.email, password="1234"))
later_today = datetime.now() + timedelta(hours=1)
earlier_today = datetime.now() - timedelta(hours=1)
past_date = datetime.now() - timedelta(days=1)
event1 = EventFactory.create(start_date=past_date, end_date=later_today)
event2 = EventFactory.create(start_date=past_date, end_date=earlier_today)
url = reverse('social:community_news')
response = self.client.get(url, data={'orders': 'works:1$'})
response = self.client.get(url)
self.assertEqual(200, response.status_code)
soup = BeautifulSoup(response.content, 'html.parser')
self.assertEqual(len(soup.select(".upcoming-events")), 1)
self.assertEqual(len(soup.select(".upcoming-events .event-summary")), 1)
self.assertContains(response, work1.title)
self.assertNotContains(response, other_post.text)
self.assertContains(response, 'Upcoming events')
self.assertContains(response, event1.title)
self.assertNotContains(response, event2.title)
def test_view_finished_today_events(self):
# Create a active user
individual = IndividualFactory.create()
user = individual.user
self.assertTrue(self.client.login(email=user.email, password="1234"))
later_today = datetime.now() + timedelta(hours=1)
earlier_today = datetime.now() - timedelta(hours=1)
past_date = datetime.now() - timedelta(days=1)
event1 = EventFactory.create(start_date=past_date, end_date=earlier_today)
url = reverse('social:community_news')
response = self.client.get(url)
self.assertEqual(200, response.status_code)
soup = BeautifulSoup(response.content, 'html.parser')
self.assertEqual(len(soup.select(".upcoming-events")), 1)
self.assertEqual(len(soup.select(".upcoming-events .event-summary")), 0)
self.assertContains(response, 'Upcoming events')
def test_view_all_types(self):
# Create a active user
individual = IndividualFactory.create()
......@@ -209,8 +249,6 @@ class CommunityNewsTest(BaseTestCase):
individual = IndividualFactory.create()
user = individual.user
self.assertTrue(self.client.login(email=user.email, password="1234"))
future_date = datetime.now() + timedelta(days=1)
today_date = datetime.now()
past_date = datetime.now() - timedelta(days=1)
event1 = EventFactory.create(start_date=past_date)
event2 = EventFactory.create(start_date=None)
......
......@@ -1101,6 +1101,27 @@ class MessageThreadTest(BaseAPITestCase):
self.assertEqual(len(response.data), 1)
self.assertTrue('iframe' in response.data[0]['html_body'])
def test_get_message_reply_youtube_short(self):
profile1 = IndividualFactory.create()
profile2 = IndividualFactory.create()
site = Site.objects.get_current()
message = MessageFactory.create(sender=profile1.user, recipients=[profile2.user])
youtube_url = 'https://youtu.be/8yDKRE2-mOs'
MessageReplyFactory.create(
message=message, sender=profile2.user, body="Listen this sound {0}".format(youtube_url)
)
self.assertTrue(self.client.login(email=profile1.user.email, password="1234"))
url = reverse('social:message_thread', args=[message.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertTrue('iframe' in response.data[0]['html_body'])
def test_get_message_reply_soundcloud(self):
profile1 = IndividualFactory.create()
......
# -*- coding: utf-8 -*-
from datetime import datetime
from datetime import datetime, date
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
......@@ -29,13 +29,11 @@ class CommunityNewsView(ListBasePageView):
EVERYONE = 'everyone'
FOLLOWING = 'following'
WORKS = 'works'
DEFAULT_ORDERING = EVERYONE
ORDER_BY_CHOICES = [
(EVERYONE, _('Everyone\'s activity')),
(FOLLOWING, _('Everyone I follow')),
(WORKS, _('Works')),
]
@method_decorator(login_required)
......@@ -56,8 +54,6 @@ class CommunityNewsView(ListBasePageView):
def order_items(self, queryset, order_by, ordering):
if order_by == self.FOLLOWING:
queryset = filter_following(queryset, self.request.user)
elif order_by == self.WORKS:
queryset = get_all_feeds(only_works=True)
else:
queryset = get_all_feeds(no_works=True)
return super(CommunityNewsView, self).order_items(queryset, order_by, ordering)
......@@ -80,14 +76,14 @@ class CommunityNewsView(ListBasePageView):
context['items'] = [elt for elt in items if not elt.is_deleted]
events = get_upcoming_events_queryset()[:4]
events = get_upcoming_events_queryset(take_time_into_account=True)[:4]
context['events'] = events
no_upcoming_events = False
if events.count():
if events[0].end_date:
no_upcoming_events = events[0].end_date < datetime.today()
no_upcoming_events = events[0].end_date.date() < date.today()
elif events[0].start_date:
no_upcoming_events = events[0].start_date < datetime.today()
no_upcoming_events = events[0].start_date.date() < date.today()
else:
no_upcoming_events = True
context['no_upcoming_events'] = no_upcoming_events
......
......@@ -13,7 +13,7 @@ from ulysses.sites import BaseAdminSite
from ulysses.competitions.models import (
Candidate, TemporaryMedia, TemporaryDocument, ApplicationDraft, CompetitionManager, JuryMember,
CandidateDocument, CandidateMedia
CandidateDocument, CandidateMedia, Competition
)
from ulysses.composers.models import Composer, Media, Document, BiographicElement
......@@ -28,7 +28,7 @@ class SuperAdminSite(BaseAdminSite):
Candidate, TemporaryMedia, TemporaryDocument, ApplicationDraft, Media, Document, BiographicElement,
)
model_classes2 = (
JuryMember, CompetitionManager
JuryMember,
)
objects_to_merge = {}
......@@ -115,6 +115,16 @@ class SuperAdminSite(BaseAdminSite):
profile.user.groups.add(group)
profile.user.save()
# add new user as manager of source user competitions
try:
manager_from = CompetitionManager.objects.get(user=user)
manager_to = CompetitionManager.objects.get_or_create(user=profile.user)[0]