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.messages',
     'django.contrib.staticfiles',
+    'django.contrib.humanize',
     'debug_toolbar',
     'pipeline',
     'crispy_forms',
@@ -227,7 +228,7 @@ MISAGO_DYNAMIC_AVATAR_DRAWER = 'misago.users.avatars.dynamic.draw_default'
 
 # For which sizes avatars should be cached?
 # 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

+ 1 - 1
misago/core/exceptionhandler.py

@@ -38,7 +38,7 @@ def handle_http404_exception(request, exception):
 def handle_outdated_slug_exception(request, exception):
     matched_url = request.resolver_match.url_name
     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_name = model.__class__.__name__.lower()

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

@@ -7,6 +7,7 @@
 @import "modals.less";
 @import "markup.less";
 @import "yesnoswitch.less";
+@import "states.less";
 
 // Layout elements
 @import "navbar.less";
@@ -18,5 +19,6 @@
 
 // Pages
 @import "errorpages.less";
+@import "profile.less";
 @import "signin.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-bottom: @line-height-computed;
 
+  .avatar-preview {
+  	max-width: 200px;
+  }
+
   .usercp-avatar-options {
     ul {
       li {

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

@@ -260,6 +260,15 @@
 @alert-danger-text:           #fff;
 
 
+//== User state
+//
+//##
+
+@state-online:                #2ecc71;
+@state-offline:               #95a5a6;
+@state-banned:                #e74c3c;
+
+
 //== 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>
     <ul class="dropdown-menu">
       <li>
-        <a href="#">
+        <a href="{% url USER_PROFILE_URL user_slug=user.slug user_id=user.pk %}">
           <span class="fa fa-user"></span>
           {% trans "See your profile" %}
         </a>

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

@@ -18,7 +18,7 @@
 
       <div class="media">
         <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>
         <div class="media-body usercp-avatar-options">
           {% if user.is_avatar_banned %}

+ 4 - 3
misago/users/apps.py

@@ -40,6 +40,7 @@ class MisagoUsersConfig(AppConfig):
                             icon='fa fa-check')
 
     def register_default_user_profile_pages(self):
-        user_profile.add_page(link='misago:index',
-                              name='Todo',
-                              icon='fa fa-check')
+        user_profile.add_page(link='misago:user_posts',
+                              name=_("Posts"))
+        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_ip = tracker.current_ip
                     user.save(update_fields=['last_login', 'last_ip'])
+                    tracker.delete()
                 else:
                     # Bump user's tracker time
                     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')),
                 ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
                 ('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_hash', models.CharField(unique=True, max_length=32)),
                 ('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
 
     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):
         return self.get(email_hash=hash_email(email))
 
     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))
         return self.get(queryset)
 
@@ -139,11 +139,11 @@ class User(AbstractBaseUser, PermissionsMixin):
     """
     Note that "username" field is purely for shows.
     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.
     """
     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:
     "email" holds normalized email address
@@ -208,7 +208,7 @@ class User(AbstractBaseUser, PermissionsMixin):
 
     is_active = True # Django's is_active means "is not deleted"
 
-    USERNAME_FIELD = 'username_slug'
+    USERNAME_FIELD = 'slug'
     REQUIRED_FIELDS = ['email']
 
     objects = UserManager()
@@ -283,7 +283,7 @@ class User(AbstractBaseUser, PermissionsMixin):
         if new_username != self.username:
             old_username = self.username
             self.username = new_username
-            self.username_slug = slugify(new_username)
+            self.slug = slugify(new_username)
 
             if self.pk:
                 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',
     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'}),

+ 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})