]> git.parisson.com Git - teleforma.git/commitdiff
Add testimonials for webconference
authorYoan Le Clanche <yoanl@pilotsystems.net>
Wed, 5 Nov 2025 13:37:23 +0000 (14:37 +0100)
committerYoan Le Clanche <yoanl@pilotsystems.net>
Wed, 5 Nov 2025 13:37:23 +0000 (14:37 +0100)
12 files changed:
teleforma/admin.py
teleforma/management/commands/teleforma-revisions-from-bbb.py
teleforma/migrations/0024_seminar_secondary_subject.py [new file with mode: 0644]
teleforma/migrations/0025_auto_20251105_1331.py [new file with mode: 0644]
teleforma/models/core.py
teleforma/models/pro.py
teleforma/static/teleforma/css/teleforma.css
teleforma/templates/teleforma/conference_testimonial.html [new file with mode: 0644]
teleforma/templates/teleforma/conference_testimonial_payback.html [new file with mode: 0644]
teleforma/templates/teleforma/testimonials.html
teleforma/urls.py
teleforma/views/pro.py

index 3cd48c1e34735eb22d0ebaa0fe65df44b1576e92..753fb6f0b18a90c7305b8b9042d828a223ad38f9 100644 (file)
@@ -262,9 +262,20 @@ class MediaItemMarkerAdmin(admin.ModelAdmin):
 
 
 class TestimonialAdmin(admin.ModelAdmin):
-    search_fields = ['seminar__course__title', 'seminar__sub_title',
+    search_fields = ['seminar__course__title', 'seminar__sub_title', 'conference__title',
                     'user__username', 'user__last_name']
-
+    list_display = ['get_title', 'user', 'date_added', 'date_modified']
+    list_filter = ['date_added', 'date_modified']
+    sortable_by = ['date_added', 'date_modified']
+
+    def get_title(self, obj):
+        """Affiche le titre de l'attestation"""
+        if obj.seminar:
+            return obj.seminar.title
+        elif obj.conference:
+            return obj.conference.title
+        return "N/A"
+    get_title.short_description = 'Titre'
 
 class AnswerAdmin(admin.ModelAdmin):
     search_fields = ['user__username', 'user__last_name']
@@ -274,10 +285,29 @@ class AnswerAdmin(admin.ModelAdmin):
 
 
 class SeminarRevisionAdmin(admin.ModelAdmin):
-    search_fields = ['user__username', 'seminar__title']
+    search_fields = ['user__username', 'seminar__title', 'conference__title']
     date_hierarchy = 'date_modified'
     readonly_fields=('date',)
-
+    list_display = ['get_title', 'user', 'date', 'date_modified', 'get_type']
+    list_filter = ['date', 'date_modified']
+
+    def get_title(self, obj):
+        """Affiche le titre du séminaire ou de la conférence"""
+        if obj.seminar:
+            return obj.seminar.title
+        elif obj.conference:
+            return obj.conference.title
+        return "N/A"
+    get_title.short_description = 'Formation'
+
+    def get_type(self, obj):
+        """Affiche le type (séminaire ou conférence)"""
+        if obj.seminar:
+            return "Séminaire"
+        elif obj.conference:
+            return "Conférence"
+        return "N/A"
+    get_type.short_description = 'Type'
 
     actions = ['export_seminar_revisions']
 
@@ -297,24 +327,34 @@ class SeminarRevisionAdmin(admin.ModelAdmin):
 
         data = {}
         for rev in queryset.order_by('-date'):
-            seminar_id = rev.seminar.id
-            if seminar_id not in data:
-                data[seminar_id] = {'title': rev.seminar.pretty_title.encode('utf-8'), 'users': {}}
+            # Gère les révisions de séminaire et de conférence
+            if rev.seminar:
+                item_id = ('seminar', rev.seminar.id)
+                title = rev.seminar.pretty_title.encode('utf-8')
+            elif rev.conference:
+                item_id = ('conference', rev.conference.id)
+                title = rev.conference.title.encode('utf-8')
+            else:
+                continue
+
+            if item_id not in data:
+                data[item_id] = {'title': title, 'users': {}}
 
             user = rev.user.email
-            if user not in data[seminar_id]['users']:
-                data[seminar_id]['users'][user] = []
+            if user not in data[item_id]['users']:
+                data[item_id]['users'][user] = []
 
             if rev.date_modified:
-                data[seminar_id]['users'][user].append({'start': rev.date, 'end': rev.date_modified})
+                data[item_id]['users'][user].append({'start': rev.date, 'end': rev.date_modified})
 
         writer = UnicodeWriter(response, delimiter=";")
-        writer.writerow(["seminaire", "email", "duree total", "date debut", "date fin", "duree"])
-        for seminar in list(data.keys()):
-            title = data[seminar]['title']
-            users = data[seminar]['users']
+        writer.writerow(["formation", "type", "email", "duree total", "date debut", "date fin", "duree"])
+        for item_key in list(data.keys()):
+            title = data[item_key]['title']
+            item_type = "Séminaire" if item_key[0] == 'seminar' else "Conférence"
+            users = data[item_key]['users']
             for user in list(users.keys()):
-                l = [title, user]
+                l = [title, item_type, user]
                 revs = users[user]
                 duration = datetime.timedelta()
                 for rev in revs:
@@ -325,7 +365,7 @@ class SeminarRevisionAdmin(admin.ModelAdmin):
                     l.append(str(rev['end']))
                     l.append(pretty_duration(rev['end'] - rev['start']) if rev['end'] else 0)
                     duration += rev['end'] - rev['start'] if rev['end'] else datetime.timedelta()
-                l.insert(2, pretty_duration(duration))
+                l.insert(3, pretty_duration(duration))
                 writer.writerow(l)
         return response
 
index 17315febe1baa653b253a9cc9a8fc12199d24b05..e0abe4322aec715458a5ceca02f47d62f469539c 100644 (file)
@@ -28,7 +28,7 @@ class Command(BaseCommand):
     def handle(self, *args, **options):
         duration = options['duration']
         logpath = options['logfile']
-        sys.stdout = open(logpath, 'a', encoding='utf-8')
+        sys.stdout = open(logpath, 'a', encoding='utf-8')
 
         end = datetime.datetime.now()
         start = end - datetime.timedelta(seconds = duration)
@@ -49,12 +49,21 @@ class Command(BaseCommand):
             meeting_id = str(meeting['meetingID'])
             try:
                 conf = Conference.objects.get(webclass_id = meeting_id)
-                seminar = Seminar.objects.get(conference = conf)
+                print(conf)
             except:
-                print(("Warning, can't find Seminar for %s" % meeting_id))
+                print(("Warning, can't find Conference for %s" % meeting_id))
                 continue
 
-            sem_txt = "%s (%s)" % (seminar, seminar.pk)
+            # Essayer de trouver un séminaire associé
+            try:
+                seminar = Seminar.objects.get(conference = conf)
+                print(seminar)
+                sem_txt = "%s (%s)" % (seminar, seminar.pk)
+            except:
+                seminar = None
+                sem_txt = "Conference %s (%s)" % (conf.title, conf.pk)
+                print(("No Seminar for %s, using Conference directly" % meeting_id))
+
             attendees = as_list(meeting['attendees']['attendee'])
             for attendee in attendees:
                 user_id = str(attendee['userID'])
@@ -71,17 +80,71 @@ class Command(BaseCommand):
                     continue
                 conf.set_as_read(user)
                 user_txt = "%s (%s)" % (user, user.pk)
-                rev = SeminarRevision.objects.filter(seminar = seminar,
-                                                     user = user,
-                                                     date_modified = None)
+
+                # Chercher une révision ouverte (soit avec seminar, soit avec conference)
+                if seminar:
+                    rev = SeminarRevision.objects.filter(seminar=seminar,
+                                                         user=user,
+                                                         date_modified=None)
+                else:
+                    rev = SeminarRevision.objects.filter(conference=conf,
+                                                         user=user,
+                                                         date_modified=None)
+
                 if rev.count():
                     print(("User %s already has an open revision on %s" % (user_txt, sem_txt)))
-                else:                    
+                else:
                     print(("Crediting %d seconds to %s on %s" % (duration, user_txt, sem_txt)))
-                    sr = SeminarRevision(seminar = seminar,
-                                         user = user,
-                                         date = start,
-                                         date_modified = end)
+                    sr = SeminarRevision(seminar=seminar,
+                                         conference=conf,
+                                         user=user,
+                                         date=start,
+                                         date_modified=end)
                     sr.save()
                     sr.date = start
                     sr.save()
+                
+                # si la durée totale de la participation est supérieure à la durée de la formation (-20 minutes), on créé une attestation
+                # calcule le temps passé en webconférence
+                total_spent = datetime.timedelta()
+
+                # Chercher toutes les révisions liées à cette conférence (via seminar ou directement)
+                if seminar:
+                    revisions = SeminarRevision.objects.filter(seminar=seminar, user=user, conference=conf)
+                else:
+                    revisions = SeminarRevision.objects.filter(conference=conf, user=user, seminar=None)
+
+                for revision in revisions:
+                    total_spent += revision.delta()
+
+                required_duration = conf.duration.as_seconds() * 0.9
+                # print("Required duration: %d seconds" % required_duration)
+                # print("Total spent: %d seconds" % total_spent.total_seconds())
+                if total_spent.total_seconds() >= required_duration:
+                    # Chercher une attestation existante
+                    if seminar:
+                        existing_testimonial = Testimonial.objects.filter(
+                            seminar=seminar,
+                            user=user,
+                            conference=conf
+                        ).first()
+                    else:
+                        existing_testimonial = Testimonial.objects.filter(
+                            conference=conf,
+                            user=user,
+                            seminar=None
+                        ).first()
+                    # print("Existing testimonial: %s" % existing_testimonial)
+
+                    if not existing_testimonial:
+                        print("Creating testimonial for %s" % conf.title + " for user " + user.username)
+                        testimonial = Testimonial(
+                            seminar=seminar,
+                            conference=conf,
+                            user=user,
+                            date_added=start,
+                            date_modified=end
+                        )
+                        testimonial.save()
+                        conf.viewers.add(user)
+                        conf.save()
diff --git a/teleforma/migrations/0024_seminar_secondary_subject.py b/teleforma/migrations/0024_seminar_secondary_subject.py
new file mode 100644 (file)
index 0000000..25afc39
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.23 on 2025-03-11 12:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('teleforma', '0023_conference_fif_pl'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='seminar',
+            name='secondary_subject',
+            field=models.CharField(blank=True, choices=[('', 'Rien'), ('article_98_1', 'Article 98-1'), ('article_98_1_oral_blanc', 'Article 98-1 - Oral blanc')], default='', max_length=50, verbose_name='Matière secondaire'),
+        ),
+    ]
diff --git a/teleforma/migrations/0025_auto_20251105_1331.py b/teleforma/migrations/0025_auto_20251105_1331.py
new file mode 100644 (file)
index 0000000..05cea7b
--- /dev/null
@@ -0,0 +1,41 @@
+# Generated by Django 3.2.23 on 2025-11-05 13:31
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('teleforma', '0024_seminar_secondary_subject'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='conference',
+            name='viewers',
+            field=models.ManyToManyField(blank=True, help_text='Utilisateurs ayant vu la webconférence', related_name='conference_viewers', to=settings.AUTH_USER_MODEL, verbose_name='viewers'),
+        ),
+        migrations.AddField(
+            model_name='seminarrevision',
+            name='conference',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='revision', to='teleforma.conference', verbose_name='conference'),
+        ),
+        migrations.AddField(
+            model_name='testimonial',
+            name='conference',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='testimonial', to='teleforma.conference', verbose_name='conference'),
+        ),
+        migrations.AlterField(
+            model_name='seminar',
+            name='secondary_subject',
+            field=models.CharField(blank=True, choices=[('', 'Aucune'), ('article_98_1', 'Article 98-1'), ('article_98_1_oral_blanc', 'Article 98-1 - Oral blanc')], default='', max_length=50, verbose_name='Matière secondaire'),
+        ),
+        migrations.AlterField(
+            model_name='seminarrevision',
+            name='seminar',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='revision', to='teleforma.seminar', verbose_name='seminar'),
+        ),
+    ]
index 97994e016eaab1b5d72759acce04bfc7190f522f..0331e39004bd87576612e795ed2f5716c93dcf5f 100755 (executable)
@@ -960,6 +960,8 @@ class Conference(Displayable, WebclassMixin, ProductCodeMixin, SuggestionsMixin)
     approved        = models.BooleanField(_('approved'), default=True)
     readers         = models.ManyToManyField(User, related_name="conference", verbose_name=_('readers'),
                                         blank=True)
+    viewers         = models.ManyToManyField(User, help_text="Utilisateurs ayant vu la webconférence", related_name="conference_viewers", verbose_name=_('viewers'),
+                                        blank=True)
     docs_description = models.ManyToManyField(Document, related_name="conference_docs_description",
                                         verbose_name=_('description documents'),
                                         blank=True)
index 187a1f5fd2f3c1bcea0104d0a77ad4541a0e5db0..38e06cf485b4d5c675d8eeb8be3bab9ea78b20da 100644 (file)
@@ -65,7 +65,10 @@ class SeminarType(models.Model):
         verbose_name = _('Seminar type')
 
 
-
+class SecondarySubjectChoices(models.TextChoices):
+    NONE = '', _("Aucune")
+    ARTICLE_98_1 = 'article_98_1', _("Article 98-1")
+    ARTICLE_98_1_ORAL_BLANC = 'article_98_1_oral_blanc', _("Article 98-1 - Oral blanc")
 
 
 class Seminar(ClonableMixin, Displayable, ProductCodeMixin, SuggestionsMixin):
@@ -74,6 +77,13 @@ class Seminar(ClonableMixin, Displayable, ProductCodeMixin, SuggestionsMixin):
     # status values : 1: draft, 2: published
     fif_pl          = models.BooleanField(_('Eligible FIF-PL'), default=False)
     new             = models.BooleanField(_('Nouveau'), default=False)
+    secondary_subject = models.CharField(
+        _("Matière secondaire"),
+        max_length=50,
+        choices=SecondarySubjectChoices.choices,
+        blank=True,
+        default=SecondarySubjectChoices.NONE
+    )
     private         = models.BooleanField(_('private'), default=False)
     upcoming        = models.BooleanField("A venir (shop)", default=False)
     type            = models.ForeignKey(SeminarType, related_name='seminar', verbose_name=_('type'),
@@ -437,6 +447,8 @@ class Testimonial(models.Model):
 
     seminar     = models.ForeignKey(Seminar, related_name="testimonial", verbose_name=_('seminar'),
                                     blank=True, null=True, on_delete=models.SET_NULL)
+    conference  = models.ForeignKey(Conference, related_name="testimonial", verbose_name=_('conference'),
+                                    blank=True, null=True, on_delete=models.SET_NULL)
     user        = models.ForeignKey(User, related_name="testimonial", verbose_name=_('user'),
                                     blank=True, null=True, on_delete=models.SET_NULL)
     template    = models.ForeignKey(TestimonialTemplate, related_name="testimonial",
@@ -448,6 +460,11 @@ class Testimonial(models.Model):
     title       = models.CharField(_('title'), max_length=255, blank=True)
     sent        = models.BooleanField("Envoyé par email ?", default=False)
 
+    @property
+    def webconf(self):
+        """Indique si l'attestation est pour une webconférence (si on a un champ conference renseigné)"""
+        return self.conference is not None
+
     def get_title(self):
         if self.date_modified:
             date = self.date_modified
@@ -541,13 +558,20 @@ class Auditor(models.Model):
 
 class SeminarRevision(models.Model):
 
-    seminar     = models.ForeignKey(Seminar, related_name="revision", verbose_name=_('seminar'), on_delete=models.CASCADE)
+    seminar     = models.ForeignKey(Seminar, related_name="revision", verbose_name=_('seminar'), blank=True, null=True, on_delete=models.CASCADE)
+    conference  = models.ForeignKey(Conference, related_name="revision", verbose_name=_('conference'), blank=True, null=True, on_delete=models.CASCADE)
     user        = models.ForeignKey(User, related_name="revision", verbose_name=_('user'), on_delete=models.CASCADE)
     date        = models.DateTimeField(_('date added'), auto_now_add=True, null=True)
     date_modified  = models.DateTimeField(_('date modified'), blank=True, null=True)
 
     def __str__(self):
-        return ' - '.join([self.seminar.title, self.user.username, str(self.date), str(self.date_modified)])
+        title = self.seminar.title if self.seminar else (self.conference.title if self.conference else "N/A")
+        return ' - '.join([title, self.user.username, str(self.date), str(self.date_modified)])
+
+    @property
+    def webconf(self):
+        """Indique si la révision est due à une webconférence (si on a un champ conference renseigné)"""
+        return self.conference is not None
 
     class Meta(MetaCore):
         db_table = app_label + '_' + 'seminar_revisions'
index 6056254d91c3b0fb4ee437ea0f4ca4d14f22f9ec..11db2ad58558d97e0a6d871de736c0cbea51ee52 100644 (file)
@@ -35,9 +35,6 @@ margin: 0 auto -58px;
 #footer {
 height: 30px;
 }
-
-
-
 /*a:link, a:visited {
     color: #BB0000;
     text-decoration:none;
diff --git a/teleforma/templates/teleforma/conference_testimonial.html b/teleforma/templates/teleforma/conference_testimonial.html
new file mode 100644 (file)
index 0000000..89fcc34
--- /dev/null
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+{% load i18n %}
+{% load teleforma_tags %}
+
+{% get_current_language as LANGUAGE_CODE %}
+{% get_available_languages as LANGUAGES %}
+<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ LANGUAGE_CODE }}" xml:lang="{{ LANGUAGE_CODE }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
+
+<head>
+<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
+
+<title>{%block head_title %}{% description %} - {% trans "Testimonial" %}{% endblock %}</title>
+
+{% block stylesheets %}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}teleforma/css/teleforma_pdf.css" />
+{% endblock %}
+
+{% block extra_stylesheets %}{% endblock %}
+
+</head>
+
+<body>
+{% block layout %}
+<div id="layout">
+
+<div id="header">
+    <span style="color: yellow; font-weight: bold;">&gt;</span>Attestation de formation
+</div>
+
+<div id="content">
+
+   <table class="table1">
+      <tr><td class="bold">{% trans "Last name" %} : </td><td>{{ testimonial.user.last_name|upper }}</td></tr>
+      <tr><td class="bold">{% trans "First name" %} : </td><td>{{ testimonial.user.first_name|upper }}</td></tr>
+      <tr><td class="bold">{% trans "Address" %} : </td><td>{{ testimonial.user.auditor.all.0.address }} {{ testimonial.user.auditor.all.0.postal_code }} {{ testimonial.user.auditor.all.0.city }}</td></tr>
+      <tr><td class="bold">Durée de la formation : </td><td>{{ conference.duration|hours }} {% trans "hours" %}</td></tr>
+      <tr><td class="bold">{% trans "Object" %} : </td><td>{{ conference.title }}</td></tr>
+      <tr><td class="bold">Type de formation : </td><td>Présentiel / Webconférence</td></tr>
+      <tr><td class="bold">Date de la formation : </td><td>{{ first_revision.date|date:'j F Y' }}</td></tr>
+    </table>
+
+    <table class="table2">
+      <tr><td>
+        {{ conference.department.address|safe }}
+      </td>
+      <td>
+        <img src="https://{{ site_url }}{{ conference.department.signature.url }}" title="Pro-Barreau signature" alt="Pro-Barreau signature" style="width:300px;" />
+      </td></tr>
+    </table>
+
+</div>
+
+{% block footer %}
+<div id="footer">
+    Copyright &copy; {% current_year %} {{ conference.department }}
+ </div>
+{% endblock %}
+
+</div>
+{% endblock layout %}
+
+</body>
+</html>
diff --git a/teleforma/templates/teleforma/conference_testimonial_payback.html b/teleforma/templates/teleforma/conference_testimonial_payback.html
new file mode 100644 (file)
index 0000000..562fcd3
--- /dev/null
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+{% load i18n %}
+{% load teleforma_tags %}
+
+{% get_current_language as LANGUAGE_CODE %}
+{% get_available_languages as LANGUAGES %}
+<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ LANGUAGE_CODE }}" xml:lang="{{ LANGUAGE_CODE }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
+
+<head>
+<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
+
+<title>{%block head_title %}{% description %} - {% trans "Testimonial" %}{% endblock %}</title>
+
+{% block stylesheets %}
+<link rel="stylesheet" type="text/css" href="/static/teleforma/css/teleforma_pdf.css" />
+{% endblock %}
+
+<style>
+
+@page :first {
+  background: url(https://{{ site.domain }}/static/teleforma/images/attestation_fifpl_v4.png) no-repeat center;
+  background-size: cover;
+  margin: 0;
+}
+
+  #user {
+    position: absolute;
+    top: 380px;
+    left: 200px;
+  }
+
+  #conference {
+    position: absolute;
+    top: 420px;
+    left: 60px;
+  }
+
+  #date_start_d {
+    position: absolute;
+    top: 460px;
+    left: 395px;
+  }
+
+ #date_start_m {
+  position: absolute;
+    top: 460px;
+    left: 430px;
+  }
+ #date_start_y {
+  position: absolute;
+    top: 460px;
+    left: 475px;
+  }
+
+  #date_end_d {
+    position: absolute;
+    top: 460px;
+    left: 595px;
+  }
+
+ #date_end_m {
+  position: absolute;
+    top: 460px;
+    left: 630px;
+  }
+ #date_end_y {
+  position: absolute;
+    top: 460px;
+    left: 675px;
+  }
+
+  #conference-semi-days {
+    position: absolute;
+    top: 530px;
+    left: 172px;
+  }
+  #conference-days {
+    position: absolute;
+    top: 530px;
+    left: 366px;
+  }
+  #conference-presentiel-hours {
+    position: absolute;
+    top: 530px;
+    left: 550px;
+  }
+  #conference-elearning-hours {
+    position: absolute;
+    top: 676px;
+    left: 172px;
+  }
+
+
+  #nb-parts {
+    position: absolute;
+    top: 676px;
+    left: 366px;
+  }
+
+
+
+  #price-ht {
+    position: absolute;
+    top: 798px;
+    left: 265px;
+  }
+
+  #price-ttc {
+    position: absolute;
+    top: 798px;
+    left: 430px;
+  }
+
+  #date-validated {
+    position: absolute;
+    top: 676px;
+    left: 550px;
+  }
+
+  #date-now {
+    position: absolute;
+    top: 920px;
+    left: 94px;
+  }
+
+
+</style>
+
+{% block extra_stylesheets %}{% endblock %}
+
+</head>
+
+<body id="conference_testimonial_payback">
+{% block layout %}
+<div id="layout">
+<div id="content" style="font-size=1.3em;">
+
+
+<div id="user">
+  {{ testimonial.user.first_name }} {{ testimonial.user.last_name }}
+</div>
+
+
+<div id="conference">
+  {{ title }}
+</div>
+
+
+
+
+<div id="date_start_d" class="date">
+  {{ first_revision.date|date:'j' }}
+</div>
+<div id="date_start_m">
+  {{ first_revision.date|date:'m' }}
+</div>
+<div id="date_start_y">
+  {{ first_revision.date|date:'Y' }}
+</div>
+
+<div id="date_end_d">
+    {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'j' }}{% else %}{{ testimonial.date_added|date:'j' }}{% endif %}
+</div>
+<div id="date_end_m">
+    {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'m' }}{% else %}{{ testimonial.date_added|date:'m' }}{% endif %}
+</div>
+<div id="date_end_y">
+    {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'Y' }}{% else %}{{ testimonial.date_added|date:'Y' }}{% endif %}
+</div>
+
+
+{% if blended %}
+<div id="conference-semi-days">
+  {{ nb_semi_days }}
+</div>
+<div id="conference-days">
+  {{ nb_days }}
+</div>
+<div id="conference-presentiel-hours">
+  {{ hours_presentiel }}
+</div>
+{% endif %}
+
+<div id="conference-elearning-hours">
+  {{ hours_elearning|hours }}
+</div>
+
+<div id="nb-parts">
+  {{ nb_parts }}
+</div>
+
+<div id="price-ht">
+  {{ price|floatformat:2 }}
+</div>
+
+<div id="price-ttc">
+  {{ price|floatformat:2 }}
+</div>
+
+
+<div id="date-now">
+  {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'j F Y' }}{% else %}{{ testimonial.date_added|date:'j F Y' }}{% endif %}
+</div>
+
+<div id="date-validated">
+  {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'j F Y' }}{% else %}{{ testimonial.date_added|date:'j F Y' }}{% endif %}
+</div>
+
+</div>
+</div>
+{% endblock layout %}
+
+</body>
+</html>
index 2aff09bcb77ff0a6ea2e760cb3602d3ea4ee92ff..1b1fe1619e90dc584e37382a59538f62be13724a 100644 (file)
  {% for testimonial in object_list %}
   <div class="course_content">
     <div class="course_subtitle">
-      <h3><img src="{{ STATIC_URL }}/teleforma/images/item_title.png" width="10px" alt="" /> {% if testimonial.seminar %}{{ testimonial.seminar.title }}{% else %}{{ testimonial.title }}{% endif %}</h3>
+      <h3><img src="{{ STATIC_URL }}/teleforma/images/item_title.png" width="10px" alt="" /> {% if testimonial.seminar %}{{ testimonial.seminar.title }}{% elif testimonial.conference %}{{ testimonial.conference.title }}{% else %}{{ testimonial.title }}{% endif %}</h3>
     </div>
 
    <table class="listing" width="100%">
     <tbody id="spacing">
 
+    {% if testimonial.seminar %}
     <tr>
      <td><a href="{% url 'teleforma-seminar-testimonial' testimonial.seminar.id %}?format=pdf" target="_blank"><img src="{{ STATIC_URL }}/teleforma/images/application-msword.png" style="vertical-align:middle" alt="" /> {% trans "Training testimonial" %}</a></td>
      <td>{% trans "validated on" %} {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'j F Y' }}{% else %}{{ testimonial.date_added|date:'j F Y' }}{% endif %}</td>
      <td><a href="{% url 'teleforma-seminar-testimonial-payback' testimonial.seminar.id %}?format=pdf" target="_blank"><img src="{{ STATIC_URL }}teleforma/images/download.png" style="vertical-align:middle" alt="" title="{% trans "Download" %}" /></a></td>
     </tr>
     {% endif %}
+    {% elif testimonial.conference %}
+    <tr>
+     <td><a href="{% url 'teleforma-conference-testimonial' testimonial.conference.id %}?format=pdf" target="_blank"><img src="{{ STATIC_URL }}/teleforma/images/application-msword.png" style="vertical-align:middle" alt="" /> {% trans "Training testimonial" %}</a></td>
+     <td>{% trans "validated on" %} {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'j F Y' }}{% else %}{{ testimonial.date_added|date:'j F Y' }}{% endif %}</td>
+     <td><a href="{% url 'teleforma-conference-testimonial-download' testimonial.conference.id %}?format=pdf"><img src="{{ STATIC_URL }}teleforma/images/download.png" style="vertical-align:middle" alt="" title="{% trans "Download" %}" /></a></td>
+    </tr>
+
+    {% if show_fifpl %}<tr>
+     <td><a href="{% url 'teleforma-conference-testimonial-payback' testimonial.conference.id %}?format=pdf" target="_blank"><img src="{{ STATIC_URL }}/teleforma/images/application-msword.png" style="vertical-align:middle" alt="" /> {% trans "Payback testimonial" %} ({% trans "only to have support of the training costs" %})</td>
+     <td>{% trans "validated on" %} {% if testimonial.date_modified %}{{ testimonial.date_modified|date:'j F Y' }}{% else %}{{ testimonial.date_added|date:'j F Y' }}{% endif %}</td>
+     <td><a href="{% url 'teleforma-conference-testimonial-payback' testimonial.conference.id %}?format=pdf" target="_blank"><img src="{{ STATIC_URL }}teleforma/images/download.png" style="vertical-align:middle" alt="" title="{% trans "Download" %}" /></a></td>
+    </tr>
+    {% endif %}
+    {% endif %}
 
     </tbody>
    </table>
index ee39bc08cf427e44de8c3853ff005f1567ac0bba..544970b9fcee04aaba513ec1c5e46619042dfc57 100644 (file)
@@ -47,7 +47,7 @@ from django.contrib.auth.views import (LoginView, LogoutView,
 from django.views import static
 from teleforma.views.core import ConferenceRecordView, ConferenceView, CourseView, HelpView, MediaTranscodedView, MediaView, get_site_url, join_webclass
 from teleforma.views.crfpa import UserLoginView, UsersCourseView, UsersIejView, UsersTrainingView, UsersView
-from teleforma.views.pro import AnswerDetailView, AnswerDetailViewTest, AnswerView, AnswersPendingView, AnswersTreatedView, QuizQuestionView, SeminarDocumentDownloadView, SeminarDocumentView, SeminarMediaView, SeminarView, SeminarsView, TestimonialDownloadView, TestimonialKnowledgeView, TestimonialListView, TestimonialPaybackView, TestimonialView, evaluation_form_detail, webclass_bbb_webhook
+from teleforma.views.pro import AnswerDetailView, AnswerDetailViewTest, AnswerView, AnswersPendingView, AnswersTreatedView, ConferenceTestimonialDownloadView, ConferenceTestimonialPaybackView, ConferenceTestimonialView, QuizQuestionView, SeminarDocumentDownloadView, SeminarDocumentView, SeminarMediaView, SeminarView, SeminarsView, TestimonialDownloadView, TestimonialKnowledgeView, TestimonialListView, TestimonialPaybackView, TestimonialView, evaluation_form_detail, webclass_bbb_webhook
 from teleforma.views.profile import ProfileView
 from teleforma.views.home import HomeView
 
@@ -157,6 +157,14 @@ urlpatterns = [
     url(r'^desk/seminars/(?P<pk>.*)/testimonial-payback/$', TestimonialPaybackView.as_view(),
                                                     name="teleforma-seminar-testimonial-payback"),
 
+    # Conference Testimonial (for conferences without seminars)
+    url(r'^desk/conferences/(?P<pk>.*)/testimonial/$', ConferenceTestimonialView.as_view(),
+                                                    name="teleforma-conference-testimonial"),
+    url(r'^desk/conferences/(?P<pk>.*)/testimonial/download/$', ConferenceTestimonialDownloadView.as_view(),
+                                                    name="teleforma-conference-testimonial-download"),
+    url(r'^desk/conferences/(?P<pk>.*)/testimonial-payback/$', ConferenceTestimonialPaybackView.as_view(),
+                                                    name="teleforma-conference-testimonial-payback"),
+
     # Postman
     url(r'^messages/', include('postman.urls')),
 
index 28950e2f71e8bca2d5c201a155e9a72be772bdf0..666721573d7039eb6335c185d8c6efc873a0b5e1 100644 (file)
@@ -816,37 +816,66 @@ class PDFTemplateResponseMixin(TemplateResponseMixin):
 def get_testimonial_context(testimonial):
     """ make it a stateless function so we can use it in django admin command to send testimonial pdf """
     context = {'testimonial': testimonial}
-    seminar = testimonial.seminar
-    context['seminar'] = seminar
     user = testimonial.user
-    revisions = SeminarRevision.objects.filter(
-            seminar=seminar, user=user).order_by('date')
-            
-    if revisions:
-        context['first_revision'] = revisions[0]
-
-    testimonials = Testimonial.objects.filter(
-        seminar=seminar, user=user)
-    if testimonials:
-        context['testimonial'] = testimonials[0]
-
-    context['hours_presentiel'] = 0
-    context['hours_elearning'] = seminar.duration
-    context['blended'] = False
-    if seminar.conference:
-        if seminar.conference.webclass and seminar.conference in user.auditor.get().conferences.all():
-            context['blended'] = True
-            context['conference'] = seminar.conference
-            context['hours_presentiel'] = (seminar.duration.as_seconds() - seminar.conference.webclass_hours_complementary.as_seconds()) / 3600
-            context['hours_elearning'] = seminar.conference.webclass_hours_complementary
-
-    hours = seminar.duration.as_seconds() / 3600
-    context['nb_parts'] = seminar.is_multipart and seminar.number_of_parts or 5
-    context['nb_days'] = int(hours / 6)
-    context['nb_semi_days'] = (hours / 3 ) % 2 >= 1 and 1 or 0
-    context['title'] = seminar.title
-    if seminar.level == "Spécialisation":
-        context['title'] = seminar.course.title + " - " + seminar.title
+
+    # Gère les attestations de séminaire et de conférence
+    if testimonial.seminar:
+        seminar = testimonial.seminar
+        context['seminar'] = seminar
+        revisions = SeminarRevision.objects.filter(
+                seminar=seminar, user=user).order_by('date')
+
+        if revisions:
+            context['first_revision'] = revisions[0]
+
+        testimonials = Testimonial.objects.filter(
+            seminar=seminar, user=user)
+        if testimonials:
+            context['testimonial'] = testimonials[0]
+
+        context['hours_presentiel'] = 0
+        context['hours_elearning'] = seminar.duration
+        context['blended'] = False
+        if seminar.conference:
+            if seminar.conference.webclass and seminar.conference in user.auditor.get().conferences.all():
+                context['blended'] = True
+                context['conference'] = seminar.conference
+                context['hours_presentiel'] = (seminar.duration.as_seconds() - seminar.conference.webclass_hours_complementary.as_seconds()) / 3600
+                context['hours_elearning'] = seminar.conference.webclass_hours_complementary
+
+        hours = seminar.duration.as_seconds() / 3600
+        context['nb_parts'] = seminar.is_multipart and seminar.number_of_parts or 5
+        context['nb_days'] = int(hours / 6)
+        context['nb_semi_days'] = (hours / 3 ) % 2 >= 1 and 1 or 0
+        context['title'] = seminar.title
+        if seminar.level == "Spécialisation":
+            context['title'] = seminar.course.title + " - " + seminar.title
+
+    elif testimonial.conference:
+        # Attestation de conférence seule (sans séminaire)
+        conference = testimonial.conference
+        context['conference'] = conference
+        revisions = SeminarRevision.objects.filter(
+                conference=conference, user=user, seminar=None).order_by('date')
+
+        if revisions:
+            context['first_revision'] = revisions[0]
+
+        testimonials = Testimonial.objects.filter(
+            conference=conference, user=user, seminar=None)
+        if testimonials:
+            context['testimonial'] = testimonials[0]
+
+        context['hours_presentiel'] = conference.duration.as_seconds() / 3600
+        context['hours_elearning'] = conference.webclass_hours_complementary if conference.webclass_hours_complementary else conference.duration
+        context['blended'] = conference.webclass and conference in user.auditor.get().conferences.all()
+
+        hours = conference.duration.as_seconds() / 3600
+        context['nb_parts'] = 5
+        context['nb_days'] = int(hours / 6)
+        context['nb_semi_days'] = (hours / 3 ) % 2 >= 1 and 1 or 0
+        context['title'] = conference.title
+
     return context
 
 
@@ -901,9 +930,14 @@ class TestimonialListView(ListView):
         testimonials = Testimonial.objects.filter(user=user)
         for testimonial in testimonials:
             if testimonial.seminar:
+                # Attestation de séminaire
                 seminar = testimonial.seminar
                 if seminar_validated(user, seminar) and (get_seminar_delta(user, seminar) >= 0 or testimonial.date_added <= REVISION_DATE_FILTER):
                     t.append(testimonial)
+            elif testimonial.conference:
+                # Attestation de conférence (webconf sans séminaire associé)
+                # Pour les conférences, pas de validation nécessaire, juste vérifier que l'attestation existe
+                t.append(testimonial)
         return t
 
     def get_context_data(self, **kwargs):
@@ -975,6 +1009,73 @@ class TestimonialPaybackView(TestimonialView):
         return super(TestimonialPaybackView, self).dispatch(*args, **kwargs)
 
 
+# Views for Conference testimonials (without associated Seminar)
+class ConferenceTestimonialView(PDFTemplateResponseMixin, DetailView):
+
+    context_object_name = "conference"
+    model = Conference
+    template_name = 'teleforma/conference_testimonial.html'
+    pdf_template_name = template_name
+    pdf_filename = 'testimonial.pdf'
+
+    def get_context_data(self, **kwargs):
+        context = super(ConferenceTestimonialView, self).get_context_data(**kwargs)
+        conference = context['conference']
+
+        testimonials = Testimonial.objects.filter(
+            conference=conference, user=self.request.user, seminar=None)
+
+        if testimonials:
+            testimonial = testimonials[0]
+            context.update(get_testimonial_context(testimonial))
+
+        return context
+
+    @method_decorator(login_required)
+    def dispatch(self, *args, **kwargs):
+        return super(ConferenceTestimonialView, self).dispatch(*args, **kwargs)
+
+
+class ConferenceTestimonialDownloadView(ConferenceTestimonialView):
+
+    pdf_filename = 'testimonial.pdf'
+
+    def get_pdf_filename(self):
+        super(ConferenceTestimonialDownloadView, self).get_pdf_filename()
+        conference = self.get_object()
+        prefix = str(_('Testimonial'))
+        filename = '_'.join([prefix, conference.title.replace(',', ' '),
+                            self.request.user.first_name, self.request.user.last_name, ])
+        filename += '.pdf'
+        return filename
+
+
+class ConferenceTestimonialPaybackView(ConferenceTestimonialView):
+
+    template_name = 'teleforma/conference_testimonial_payback.html'
+    pdf_template_name = template_name
+
+    def get_context_data(self, **kwargs):
+        context = super(ConferenceTestimonialPaybackView,
+                        self).get_context_data(**kwargs)
+        conference = context['conference']
+        context['price'] = conference.price
+        # recompute price if use has used a promo code
+        for cart in Cart.objects.filter(user=self.request.user, status=Cart.STATE_PAYMENT_ACCEPTED):
+            if cart.has_item(conference):
+                if cart.promo_code and conference in cart.promo_code.conferences.all():
+                    context['price'] = conference.price - \
+                        (conference.price * cart.promo_code.reduction / 100.0)
+                    break
+
+        context['site'] = Site.objects.get_current()
+        return context
+
+    @method_decorator(login_required)
+    def dispatch(self, *args, **kwargs):
+        return super(ConferenceTestimonialPaybackView, self).dispatch(*args, **kwargs)
+
+
 class QuizQuestionView(SeminarAccessMixin, SeminarRevisionMixin, QuizTake):
 
     template_name = 'quiz/question.html'