Browse Source

WIP profile page

Rafał Pitoń 11 years ago
parent
commit
b88434e4b3

+ 2 - 1
misago/conf/defaults.py

@@ -106,6 +106,7 @@ INSTALLED_APPS = (
     'django.contrib.sessions',
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django.contrib.staticfiles',
+    'django.contrib.humanize',
     'debug_toolbar',
     'debug_toolbar',
     'pipeline',
     'pipeline',
     'crispy_forms',
     'crispy_forms',
@@ -227,7 +228,7 @@ MISAGO_DYNAMIC_AVATAR_DRAWER = 'misago.users.avatars.dynamic.draw_default'
 
 
 # For which sizes avatars should be cached?
 # For which sizes avatars should be cached?
 # Keep sizes ordered from greatest to smallest
 # Keep sizes ordered from greatest to smallest
-MISAGO_AVATARS_SIZES = (150, 100, 64, 50, 30, 20)
+MISAGO_AVATARS_SIZES = (400, 200, 150, 100, 64, 50, 30, 20)
 
 
 
 
 # X-Sendfile
 # X-Sendfile

+ 1 - 1
misago/core/exceptionhandler.py

@@ -38,7 +38,7 @@ def handle_http404_exception(request, exception):
 def handle_outdated_slug_exception(request, exception):
 def handle_outdated_slug_exception(request, exception):
     matched_url = request.resolver_match.url_name
     matched_url = request.resolver_match.url_name
     if request.resolver_match.namespace:
     if request.resolver_match.namespace:
-        matched_url = '%s:%s' % (request.resolver_match, matched_url)
+        matched_url = '%s:%s' % (request.resolver_match.namespace, matched_url)
 
 
     model = exception.args[0]
     model = exception.args[0]
     model_name = model.__class__.__name__.lower()
     model_name = model.__class__.__name__.lower()

+ 2 - 0
misago/static/misago/css/misago/misago.less

@@ -7,6 +7,7 @@
 @import "modals.less";
 @import "modals.less";
 @import "markup.less";
 @import "markup.less";
 @import "yesnoswitch.less";
 @import "yesnoswitch.less";
+@import "states.less";
 
 
 // Layout elements
 // Layout elements
 @import "navbar.less";
 @import "navbar.less";
@@ -18,5 +19,6 @@
 
 
 // Pages
 // Pages
 @import "errorpages.less";
 @import "errorpages.less";
+@import "profile.less";
 @import "signin.less";
 @import "signin.less";
 @import "usercp.less";
 @import "usercp.less";

+ 60 - 0
misago/static/misago/css/misago/profile.less

@@ -0,0 +1,60 @@
+//
+// User Profile
+// --------------------------------------------------
+
+
+// Side avatar
+//
+//==
+.user-profile {
+  .profile-side {
+    .user-avatar {
+      background-color: #fff;
+      border-radius: @border-radius-large;
+      .box-shadow(0px 0px 0px 2px darken(@body-bg, 5%));
+      padding: 4px;
+    }
+  }
+}
+
+
+// Side username
+//
+//==
+.user-profile {
+  .profile-side {
+    .user-name {
+      font-size: @font-size-base * 1.8;
+    }
+  }
+}
+
+
+// Side details
+//
+//==
+.user-profile {
+  .profile-side {
+    .user-details {
+      li {
+        padding-bottom: @line-height-computed / 3;
+
+        color: fadeOut(@text-color, 40%);
+        font-size: @font-size-large;
+
+        a:link, a:visited {
+          color: fadeOut(@text-color, 40%);
+        }
+
+        a:hover {
+          color: fadeOut(@text-color, 15%);
+          text-decoration: none;
+        }
+
+        a:active {
+          color: @text-color;
+        }
+      }
+    }
+  }
+}

+ 16 - 0
misago/static/misago/css/misago/states.less

@@ -0,0 +1,16 @@
+//
+// Activity States
+// --------------------------------------------------
+
+
+.user-online {
+  color: @state-online;
+}
+
+.user-offline {
+  color: @state-offline;
+}
+
+.user-banned {
+  color: @state-banned;
+}

+ 4 - 0
misago/static/misago/css/misago/usercp.less

@@ -12,6 +12,10 @@
   padding-top: @line-height-computed;
   padding-top: @line-height-computed;
   padding-bottom: @line-height-computed;
   padding-bottom: @line-height-computed;
 
 
+  .avatar-preview {
+  	max-width: 200px;
+  }
+
   .usercp-avatar-options {
   .usercp-avatar-options {
     ul {
     ul {
       li {
       li {

+ 9 - 0
misago/static/misago/css/misago/variables.less

@@ -260,6 +260,15 @@
 @alert-danger-text:           #fff;
 @alert-danger-text:           #fff;
 
 
 
 
+//== User state
+//
+//##
+
+@state-online:                #2ecc71;
+@state-offline:               #95a5a6;
+@state-banned:                #e74c3c;
+
+
 //== Modals
 //== Modals
 //
 //
 //##
 //##

+ 77 - 0
misago/templates/misago/profile/base.html

@@ -0,0 +1,77 @@
+{% extends "misago/base.html" %}
+{% load humanize i18n misago_avatars misago_capture %}
+
+
+{% block title %}
+{{ profile.username }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block content %}
+<div class="container user-profile">
+  <div class="row">
+    <div class="col-md-3 profile-side">
+
+      <div class="user-avatar">
+        {% if authenticateds_profile %}
+        <a href="{% url 'misago:usercp_change_avatar' %}" class="tooltip-top" title="{% trans "Change your avatar" %}">
+          <img src="{{ profile|avatar:400 }}" class="img-rounded img-responsive" alt="{% trans "Avatar" %}">
+        </a>
+        {% else %}
+        <img src="{{ profile|avatar:400 }}" class="img-rounded img-responsive" alt="{% trans "Avatar" %}">
+        {% endif %}
+      </div>
+
+      <h1 class="user-name">{{ profile.username }}</h1>
+
+      <div class="user-details">
+        <ul class="list-unstyled">
+          {% if show_email %}
+          <li class="user-email">
+            <a href="mailto:{{ profile.email }}" class="tooltip-top"title="{% trans "E-mail address" %}">
+              <span class="fa fa-envelope-o fa-fw"></span>
+              {{ profile.email }}
+            </a>
+          </li>
+          {% endif %}
+          <li class="user-active">
+            {% if state.is_banned %}
+              <span class="fa fa-lock fa-fw user-banned"></span>
+              {% if state.banned_until %}
+              {% blocktrans trimmed with ban_date=state.banned_until|date %}
+                Banned until {{ ban_date }}
+              {% endblocktrans %}
+              {% else %}
+              {% trans "Banned" %}
+              {% endif %}
+            {% elif state.is_online %}
+              <span class="fa fa-circle fa-fw user-online"></span>
+              {% trans "Online" %}
+            {% else %}
+              {% capture trimmed as last_seen %}
+              <abbr class="tooltip-top dynamic time-ago" title="{{ state.last_click }}" data-timestamp="{{ state.last_click|date:"c" }}">
+                {{ state.last_click|date }}
+              </abbr>
+              {% endcapture %%}
+              <span class="fa fa-circle-o fa-fw user-offilne"></span>
+              {% blocktrans trimmed with last_seen=last_seen|safe %}
+                Seen {{ last_seen }}
+              {% endblocktrans %}
+            {% endif %}
+          </li>
+          <li class="user-joined-on">
+            <span class="tooltip-top" title="{% trans "Joined on" %}">
+              <span class="fa fa-clock-o fa-fw"></span>
+              {{ profile.joined_on|date }}
+            </span>
+          </li>
+        </ul>
+      </div>
+
+    </div>
+    <div class="col-md-9">
+      {% block page %}{% endblock page %}
+    </div>
+  </div>
+</div>
+{% endblock content %}

+ 6 - 0
misago/templates/misago/profile/posts.html

@@ -0,0 +1,6 @@
+{% extends "misago/profile/base.html" %}
+
+
+{% block page %}
+Hello, I am WIP posts list.
+{% endblock page %}

+ 6 - 0
misago/templates/misago/profile/threads.html

@@ -0,0 +1,6 @@
+{% extends "misago/profile/base.html" %}
+
+
+{% block page %}
+Hello, I am WIP threads list.
+{% endblock page %}

+ 1 - 1
misago/templates/misago/user_nav.html

@@ -8,7 +8,7 @@
     </a>
     </a>
     <ul class="dropdown-menu">
     <ul class="dropdown-menu">
       <li>
       <li>
-        <a href="#">
+        <a href="{% url USER_PROFILE_URL user_slug=user.slug user_id=user.pk %}">
           <span class="fa fa-user"></span>
           <span class="fa fa-user"></span>
           {% trans "See your profile" %}
           {% trans "See your profile" %}
         </a>
         </a>

+ 1 - 1
misago/templates/misago/usercp/change_avatar.html

@@ -18,7 +18,7 @@
 
 
       <div class="media">
       <div class="media">
         <span class="pull-left">
         <span class="pull-left">
-          <img class="media-object img-rounded" src="{{ user|avatar:avatar_size }}" width="{{ avatar_size }}" height="{{ avatar_size }}" alt="{% trans "Your current avatar" %}">
+          <img class="media-object img-rounded img-responsive avatar-preview" src="{{ user|avatar:avatar_size }}" width="{{ avatar_size }}" height="{{ avatar_size }}" alt="{% trans "Your current avatar" %}">
         </span>
         </span>
         <div class="media-body usercp-avatar-options">
         <div class="media-body usercp-avatar-options">
           {% if user.is_avatar_banned %}
           {% if user.is_avatar_banned %}

+ 4 - 3
misago/users/apps.py

@@ -40,6 +40,7 @@ class MisagoUsersConfig(AppConfig):
                             icon='fa fa-check')
                             icon='fa fa-check')
 
 
     def register_default_user_profile_pages(self):
     def register_default_user_profile_pages(self):
-        user_profile.add_page(link='misago:index',
+        user_profile.add_page(link='misago:user_posts',
-                              name='Todo',
+                              name=_("Posts"))
-                              icon='fa fa-check')
+        user_profile.add_page(link='misago:user_threads',
+                              name=_("Threads"))

+ 1 - 0
misago/users/middleware.py

@@ -47,6 +47,7 @@ class OnlineTrackerMiddleware(object):
                     user.last_login = tracker.last_click
                     user.last_login = tracker.last_click
                     user.last_ip = tracker.current_ip
                     user.last_ip = tracker.current_ip
                     user.save(update_fields=['last_login', 'last_ip'])
                     user.save(update_fields=['last_login', 'last_ip'])
+                    tracker.delete()
                 else:
                 else:
                     # Bump user's tracker time
                     # Bump user's tracker time
                     tracker.current_ip = request._misago_real_ip
                     tracker.current_ip = request._misago_real_ip

+ 1 - 1
misago/users/migrations/0001_initial.py

@@ -24,7 +24,7 @@ class Migration(migrations.Migration):
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
                 ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
                 ('username', models.CharField(max_length=30)),
                 ('username', models.CharField(max_length=30)),
-                ('username_slug', models.CharField(unique=True, max_length=30)),
+                ('slug', models.CharField(unique=True, max_length=30)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
                 ('joined_on', models.DateTimeField(default=django.utils.timezone.now, verbose_name='joined on')),
                 ('joined_on', models.DateTimeField(default=django.utils.timezone.now, verbose_name='joined on')),

+ 6 - 6
misago/users/models/user.py

@@ -124,13 +124,13 @@ class UserManager(BaseUserManager):
             return user
             return user
 
 
     def get_by_username(self, username):
     def get_by_username(self, username):
-        return self.get(username_slug=slugify(username))
+        return self.get(slug=slugify(username))
 
 
     def get_by_email(self, email):
     def get_by_email(self, email):
         return self.get(email_hash=hash_email(email))
         return self.get(email_hash=hash_email(email))
 
 
     def get_by_username_or_email(self, login):
     def get_by_username_or_email(self, login):
-        queryset = models.Q(username_slug=slugify(login))
+        queryset = models.Q(slug=slugify(login))
         queryset = queryset | models.Q(email_hash=hash_email(login))
         queryset = queryset | models.Q(email_hash=hash_email(login))
         return self.get(queryset)
         return self.get(queryset)
 
 
@@ -139,11 +139,11 @@ class User(AbstractBaseUser, PermissionsMixin):
     """
     """
     Note that "username" field is purely for shows.
     Note that "username" field is purely for shows.
     When searching users by their names, always use lowercased string
     When searching users by their names, always use lowercased string
-    and username_slug field instead that is normalized around DB engines
+    and slug field instead that is normalized around DB engines
     differences in case handling.
     differences in case handling.
     """
     """
     username = models.CharField(max_length=30)
     username = models.CharField(max_length=30)
-    username_slug = models.CharField(max_length=30, unique=True)
+    slug = models.CharField(max_length=30, unique=True)
     """
     """
     Misago stores user email in two fields:
     Misago stores user email in two fields:
     "email" holds normalized email address
     "email" holds normalized email address
@@ -208,7 +208,7 @@ class User(AbstractBaseUser, PermissionsMixin):
 
 
     is_active = True # Django's is_active means "is not deleted"
     is_active = True # Django's is_active means "is not deleted"
 
 
-    USERNAME_FIELD = 'username_slug'
+    USERNAME_FIELD = 'slug'
     REQUIRED_FIELDS = ['email']
     REQUIRED_FIELDS = ['email']
 
 
     objects = UserManager()
     objects = UserManager()
@@ -283,7 +283,7 @@ class User(AbstractBaseUser, PermissionsMixin):
         if new_username != self.username:
         if new_username != self.username:
             old_username = self.username
             old_username = self.username
             self.username = new_username
             self.username = new_username
-            self.username_slug = slugify(new_username)
+            self.slug = slugify(new_username)
 
 
             if self.pk:
             if self.pk:
                 self.namechanges.create(old_username=old_username)
                 self.namechanges.create(old_username=old_username)

+ 34 - 0
misago/users/online.py

@@ -0,0 +1,34 @@
+from datetime import timedelta
+
+from django.utils import timezone
+
+from misago.users.bans import get_user_ban
+from misago.users.models import Online
+
+
+ACTIVITY_CUTOFF = timedelta(minutes=15)
+
+
+def state_for_acl(user, acl):
+    user_state = {
+        'is_banned': False,
+        'banned_until': False,
+        'is_online': False,
+        'is_hidden': user.is_hiding_presence,
+        'last_click': user.last_login,
+    }
+
+    user_ban = get_user_ban(user)
+    if user_ban:
+        user_state['is_banned'] = True
+        user_state['banned_until'] = user_ban.valid_until
+
+    try:
+        online_tracker = user.online_tracker
+        if online_tracker.last_click >= timezone.now() - ACTIVITY_CUTOFF:
+            user_state['is_online'] = True
+            user_state['last_click'] = online_tracker.last_click
+    except Online.DoesNotExist:
+        pass
+
+    return user_state

+ 5 - 0
misago/users/urls.py

@@ -51,6 +51,11 @@ urlpatterns += patterns('misago.users.views.usercp',
 )
 )
 
 
 
 
+urlpatterns += patterns('misago.users.views.profile',
+    url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/$', 'user_posts', name="user_posts"),
+    url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/threads/$', 'user_threads', name="user_threads"),
+)
+
 urlpatterns += patterns('misago.users.views.avatarserver',
 urlpatterns += patterns('misago.users.views.avatarserver',
     url(r'^user-avatar/(?P<size>\d+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar', name="user_avatar"),
     url(r'^user-avatar/(?P<size>\d+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar', name="user_avatar"),
     url(r'^user-avatar/tmp:(?P<token>[a-zA-Z0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', name="user_avatar_tmp", kwargs={'type': 'tmp'}),
     url(r'^user-avatar/tmp:(?P<token>[a-zA-Z0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', name="user_avatar_tmp", kwargs={'type': 'tmp'}),

+ 52 - 0
misago/users/views/profile.py

@@ -0,0 +1,52 @@
+from django.contrib.auth import get_user_model
+from django.shortcuts import redirect, render as django_render
+
+from misago.core.shortcuts import get_object_or_404, validate_slug
+
+from misago.users import online
+
+
+def profile_view(f):
+    def decorator(*args, **kwargs):
+        relations = ('online_tracker', 'ban_cache')
+        queryset = get_user_model().objects.select_related(*relations)
+        profile = get_object_or_404(queryset, id=kwargs.pop('user_id'))
+
+        validate_slug(profile, kwargs.pop('user_slug'))
+        kwargs['profile'] = profile
+
+        return f(*args, **kwargs)
+    return decorator
+
+
+def render(request, template, context=None):
+    context = context or {}
+
+    if request.user.is_authenticated():
+        authenticateds_profile = context['profile'].pk == request.user.pk
+    else:
+        authenticateds_profile = False
+    context['authenticateds_profile'] = authenticateds_profile
+
+    user_acl = request.user.acl
+    if request.user.is_authenticated():
+        if authenticateds_profile:
+            context['show_email'] = True
+        else:
+            context['show_email'] = user_acl['can_see_users_emails']
+    else:
+        context['show_email'] = False
+
+    context['state'] = online.state_for_acl(context['profile'], user_acl)
+
+    return django_render(request, template, context)
+
+
+@profile_view
+def user_posts(request, profile=None, page=0):
+    return render(request, 'misago/profile/posts.html', {'profile': profile})
+
+
+@profile_view
+def user_threads(request, profile=None, page=0):
+    return render(request, 'misago/profile/threads.html', {'profile': profile})