위 포스팅은 다음 링크의 글을 번역한 것입니다. 

How to Create Django Signals





Django Signals은 특정 이벤트가 발생할 때 응용 프로그램에 알릴 수있는 전략입니다. 특정 모델 인스턴스가 업데이트 될 때마다 캐시 페이지를 무효화하려는 경우 코드 기반에서, 이 모델을 업데이트 할 수있는 위치가 여러곳 있다고 가정해 보겠습니다. 이 특정 모델의 save메소드가 실행이 될 때마다 실행될 코드 일부를 연결하여 Signals를 사용하여 이를 수행 할 수 있습니다.


 또 다른 일반적인 사용 사례는 일대일 관계를 통해 프로필 전략을 사용하여 사용자 Custom Django 사용자를 확장 한 경우입니다. 우리가 일반적으로하는 일은 "Signal dispatcher"를 사용하여 사용자의 post_save 이벤트를 수신하여 프로필 인스턴스도 업데이트하는 것입니다. 다른 게시물에서이 사례를 다뤘습니다. 여기에서 읽을 수 있습니다 : 장고 사용자 모델을 확장하는 방법.


이 튜토리얼에서는 기본 제공 신호를 소개하고 모범 사례에 대한 일반적인 조언을 제공합니다.




언제 사용해야 하는가 ?


우리가 더 나아가기 전에, 우리가 언제 사용하는지 알아야 합니다. 


- 많은 코드의 조각들이 같은 이벤트에 연결되어 있을때 (interested 를 연결로 의역) 

- 분리된 어플리케이션들의 상호작용이 필요할때

      - A Django Core model

- A model Defined by a third-part app 





어떻게 작동하는가 ?


Observer Design Pattern에 익숙하다면, 장고가 그것을 어떻게 구현하는지 알 수 있습니다. 동일한 목적으로 사용됩니다.


Signals 시스템에는 '송신자(senders)'와 '수신자(receivers)'라는 두 가지 핵심 요소가 있습니다. 이름에서 알 수 있듯이 Senders는 신호를 전달하는 책임자이고 Receivers 는이 신호를 수신 한 다음 무언가를 수행하는 객체입니다.


- Receivers는 신호를 수신하는 함수 또는 인스턴스 메소드이여야 합니다.

- Senders는 Sender로 부터 이벤트를 받으려면 Python 개체이거나 None이어야합니다.


Sender와 Receiver 사이의 연결은 connect 메소드를 통해 Signal의 인스턴스 인 "signal dispatcher"를 통해 수행됩니다.


Django Core는 또한 ModelSignal을 정의합니다.이 ModelSignal은 Sender가 app_label.ModelName 형식의 문자열로 지연 지정되도록 허용하는 Signal의 하위 클래스입니다. 하지만 일반적으로 Signal 클래스를 사용하여 사용자 정의 신호를 생성하려고합니다.


따라서 신호를 수신하려면 Signal.connect () 메서드를 사용하여 신호를 보낼 때 호출되는 수신기 함수를 등록해야합니다.





사용법


post_save 의 built-in Signal 에 대해 살펴 보겠습니다. 그 코드는 django.db.models.signals 모듈에 있습니다. 이 특별한 Signal은 save 메소드를 실행한 모델이 끝난 직후에 발생합니다.

from django.contrib.auth.models import User
from django.db.models.signals import post_save

def save_profile(sender, instance, **kwargs):
    instance.profile.save()

post_save.connect(save_profile, sender=User)

위 예제에서 save_profile은 리시버 함수이고, User는 보낸 사람이고 post_save는 신호입니다. 사용자 인스턴스가 save 메소드의 실행을 완료 할 때마다 save_profile 함수가 실행됩니다.


post_save.connect (save_profile)와 같이 송신자 인자를 입력하지 않는다면, save_profile 함수는 Django 모델이 save 메소드를 실행한 후에 실행될 것입니다.


신호를 등록하는 또 다른 방법은 @receiver 데코레이터를 사용하는 것입니다.

def receiver(signal, **kwargs)

signal 매개 변수는 Signal 인스턴스 또는 Signal 인스턴스의 List/Tuple 일 수 있습니다.


아래 예를 참조하십시오.

from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()


수신기 기능을 여러 신호에 등록하려면 다음과 같이하십시오.

@receiver([post_save, post_delete], sender=User)



코드는 어디에 있어야합니까?


응용 프로그램의 signal를 등록하는 위치에 따라 코드를 가져 오기 때문에 몇 가지 부작용이 발생할 수 있습니다. 따라서 모델 모듈 또는 응용 프로그램 루트 모듈에 두는 것을 피하는 것이 좋습니다.


Django 문서는 앱 구성 파일에 신호를 넣도록 조언합니다. profile이라는 Django 앱을 고려할 때 평소에하는 작업입니다


profiles/signals.py :


from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

from cmdbox.profiles.models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

profiles/app.py :

from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _

class ProfilesConfig(AppConfig):
    name = 'cmdbox.profiles'
    verbose_name = _('profiles')

    def ready(self):
        import cmdbox.profiles.signals  # noqa

profiles/__init__.py :

default_app_config = 'cmdbox.profiles.apps.ProfilesConfig'

위 예제에서 @receiver () 데코레이터를 사용하기 때문에 ready () 메서드 내에서 signals 모듈을 가져 오는 것만으로 작동합니다. connect () 메서드를 사용하여 리시버 함수를 연결하는 경우 아래 예제를 참조하십시오.

from cmdbox.profiles.models import Profile

def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

profiles/signals.py:

from django.apps import AppConfig
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _

from cmdbox.profiles.signals import create_user_profile, save_user_profile

class ProfilesConfig(AppConfig):
    name = 'cmdbox.profiles'
    verbose_name = _('profiles')

    def ready(self):
        post_save.connect(create_user_profile, sender=User)
        post_save.connect(save_user_profile, sender=User)

Note : INSTALLED_APPS 설정에서 이미 AppConfig를 참조하고 있다면 profiles / __ init__.py 세팅은 필요하지 않습니다.

default_app_config = 'cmdbox.profiles.apps.ProfilesConfig'


그 외 내장 Signal 함수를 찾고 싶다면 장고 문서를 참조해 주세요.


저번에는 순환참조가 말썽이더니 이번에는 url관련.. 문제가 저를 괴롭히네요.

일단 에러 메세지는 다음과 같이 나옵니다. 

분명 매칭 url이 없을때 NoreverseMatch에러가 나오는 것으로 알고 있는데... 설정은 잘 맞게 한것 같은데 해결이 안됩니다.



에러메세지는 다음과 같습니다.





Models.py
from django.db import models
from django.urls import reverse
from django.db.models.signals import pre_save, post_save
from django.utils.text import slugify

class CompanyModel(models.Model):
    cmp_name = models.CharField(max_length=100)
    cmp_descript = models.TextField()
    cmp_email = models.EmailField()
    slug = models.SlugField(max_length=128)
    updated = models.DateTimeField(auto_now_add=True, auto_now=False)

    def __str__(self):
        return self.cmp_name

    def get_absolute_url(self): # reverse 함수 설정 *************
        return reverse('company_detail', kwargs={"slug":self.slug})
Views.py
from django.shortcuts import render
from .forms import CompanyForm
from .models import CompanyModel
from django.contrib.auth.decorators import login_required
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse


class CmpCreate(CreateView):
    form_class = CompanyForm
    template_name = "create.html"
    fields = ["cmp_name", "cmp_email", c"cmp_descript"]

    def get_success_url(self):
        return reverse("company-list")

class CompanyUpdateView(UpdateView):
    form_class = CompanyForm
    template_name = "create.html"

class CmpDetail(DetailView): #DetailView 설정
    model = CompanyModel

class CmpListView(ListView):
    model = CompanyModel
urls.py
from django.views.generic.base import TemplateView
from .views import DashboardTemplateView, CmpDetail, CmpListView, CmpCreate, CompanyUpdateView
from django.conf.urls import include
from django.urls import path
from . import views

#보기 편하게 하기 위해서 나머지 불필요한 url은 모두 주석처리 했습니다. 
urlpatterns = [
    #path('', views.home, name='home'),
    #path('about/', TemplateView.as_view(template_name="about.html"), name="about"),
    # path('about2/', DashboardTemplateView.as_view(), name='dashboard'),
    #path('company/create/', CmpCreate.as_view(), name="company-create"),
    #path('company/', CmpListView.as_view(), name="company-list"),
    path('company//', CmpDetail.as_view(), name="company_detail"), #디테일 뷰 slug를 찾지 못함. ********
    #path('company//update', CompanyUpdateView.as_view(), name="company_update"),
    #path('login/', TemplateView.as_view(template_name="login.html"), name="login"),
    #path('accounts/', include('registration.backends.default.urls')),
    #path('contact/', views.contact, name="contact")
]

분명 get_absolute_url 을 설정하고 그것을 자연스럽게 urls.py로 넘겨줬는데 왜 ReverseMatch 에러가 나는지 잘 이해를 하지 못하겠습니다... 도대체... 왜 그럴까요..ㅠㅜ




문제해결..


이번 문제도 너무나도 간단하게 풀려버렸습니다. askDjango의 이진석 님께서 조언을 해주셨는데요. 


Slug 필드를 확인을 해보았습니다. 



위 사진과 같이 SLUG 필드가 한개 빠져있더군요.


영문으로 쓰면 그것을 slugify로 변환을해서 저장을 하는데... 한글은 그게 안되는 것이였음.

하하.... 그래서 slug를 가져오지 못해서 에러가 나더군요. 


만약 한글로 저장을 하게 된다면 숫자로라도 Slug필드를 넣어야 겠습니다. 읕...

오늘 하루종일 찾다가 질문을 올립니다. 
Forms.py에 2번째 라인 Model 을 import 하는 부분에서 계속 적으로 에러가 발생합니다. 
잘 작동되다가 어느순간부터 importerror가 나는데, 쉽게 잡힐줄 알았던 에러가... 답이 안보이네요.
* 파일들은 모두 같은 디렉터리 안에 있습니다. 

Forms.py // 2번째 라인 문제

from django import forms from . models import CompanyModel #CompanyModel이 임포트가 안됨. class CompanyForm(forms.ModelForm): class Meta: model = CompanyModel fields = ["cmp_name", "cmp_email", "cmp_descript", "slug"] def clean_cmp_name(self): cmp_name = self.cleaned_data.get('cmp_name') # write validation code. return cmp_name

Models.py 
from django.db import models
from django.urls import reverse
from django.db.models.signals import pre_save, post_save
from django.utils.text import slugify
from .views import CmpDetail

# Create your models here.

class CompanyModel(models.Model):
    cmp_name = models.CharField(max_length=100)
    cmp_descript = models.TextField()
    cmp_email = models.EmailField()
    slug = models.SlugField()
    updated = models.DateTimeField(auto_now_add=True, auto_now=False)

    class Meta:
        odering = ["-updated"]

    def __str__(self):
        return self.cmp_name

    def get_absolute_url(self):
        return reverse("CmpDetail", args={"slug":self.slug})

View.py

from django.shortcuts import render
from .forms import CompanyForm
from .models import CompanyModel
from django.contrib.auth.decorators import login_required
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView


class CmpDetail(DetailView):
    model = CompanyModel

class CmpListView(ListView):
    model = CompanyModel

    def get_queryset(self, *args, **kwargs):
        qs = super(CmpListView, self).get_queryset(*args, **kwargs)
        print(qs)
        print(qs.first())
        return qs

ERRORPAGE : Models.py가 임포트가 안된다는것 같은데... 답이 없네요. 

  

 File "", line 994, in _gcd_import
  File "", line 971, in _find_and_load
  File "", line 955, in _find_and_load_unlocked
  File "", line 665, in _load_unlocked
  File "", line 678, in exec_module
  File "", line 219, in _call_with_frames_removed
  File "c:\Projects\keras_talk\My_Django_Stuff\djmod\company\models.py", line 5, in 
    from .views import CmpDetail
  File "c:\Projects\keras_talk\My_Django_Stuff\djmod\company\views.py", line 2, in 
    from .forms import CompanyForm
  File "c:\Projects\keras_talk\My_Django_Stuff\djmod\company\forms.py", line 2, in 
    from . models import CompanyModel
ImportError: cannot import name 'CompanyModel'

좋은 의견 있으면 부탁드립니다. 





AskDjango의 이진석 님께서 답변을 주셨습니다. 

에러는 생각 이상으로 쉽게 잡혔네요... 


Models.py에 


from .views import CmpDetail 부분을 참조 시킨것에서 문제가 발생 했습니다. 

순환참조라고 하는데... 파이썬에서는 기능들을 이것저것 막 가져다 붙이면 문제가 발생하는 군요.. ㅋ 


혹시 ImportError가 발생하였다면 모델.py에 필요없는 부분이 임포트가 되었는지 확인해 보시기 바랍니다. 


from .views import CmpDetail




ModuleNotFoundError: No module named 'helloeb2.settings'





AWS 나 다른 환경으로 배포하기 위해 settings.py 를 분기시켜야 한다. 


그런데 말입니다. 



1. 기존 settins.py 외 settings 폴더를 생성한다.

2. settings 폴더내 common, dev, prod 파일을 생성한다. 

3. 이후 common 파일에 기존 settings 파일의 내용을 복사하고 settings 파일 삭제

4.  최상단 manage.py 의 settings 경로를 settings/dev 로 수정한다. 



그리고 


ModuleNotFoundError: No module named 'helloeb2.settings'


이 에러가 뜬다. 

분명 다른 사람들은 이렇게 하면 잘만 되던데 왜 나만... ㅅㅂ... 


분명 manage.py에서 수정을 해줬는데도  기존 settings.py을 찾는 이유는 뭘까 ? 

음 ... 몇군데 찾아보니 이유에 대한 원인을 몇가지 찾을 수 있었다. 


1., 분기전 migrations을 하면서 데이터 저장 부분이 settings를 참조하고 있거나

=> 다른 프로젝트를 만들어서 똑같이 해봤지만 에러. 마이그레이션 문제는 아닌듯 하다.


2. 상위 루트 폴더인 helloeb2  부분을 헷갈려서 에러가 나거나

=> 상위 폴더 이름을 변경해봤는데 그래도 오류가 난다. 


3. 장고 버전이 안맞아서 그런다 ?

=> 장고 2.0으로 업데이트 했으나 같은 현상이 일어났다. 





해결책. 



근데 해결은 비교적 간단하게.. . 이렇게 쉽게 풀리다니


os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'helloeb2.settings.dev')

에서


os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'settings.dev')


이렇게 하니 작동을 한다. 


어떤 이유인지는 모르겠으나 상위 디렉토리를 제거하면 된다. 

하... 2시간 날렸네.. 






나의 삽질로 남들의 시간이 단축 될 수 있다면 ... 

글 보고 해결되셨다면 댓글 하나씩만 남겨주세요. 



최근 장고라는 프레임 워크로 갈아타면서 엄청나게 삽질을 해대고 있는 중이다.. 


장고프레임 워크는 파이썬을 웹으로 만들어 줄수 있는 툴인데, 내가 그동안 해왔던 자바, C++과는 또 다른 매력이 있는 소프트웨어인듯 하다. 



지금 내가 공부를 하고 있는 방법은 Udemy와 Nomade 라는 사이트를 이용해서 공부를 하고 있는데, 정말 ... 쉽지 않다. 


기본적인 웹상의 구조를 알고 있음에도 불구하고 너무도 다른 양식의 툴을 접하니..  지금 10일째 삽질만 ... 





1. 프로젝트 진행을 배울 수 있는 좋은 강의:  Udemy.com


하지만 자세히 이것들이 어떻게 동작이 되고 왜 이렇게 되는지는 강의를 들어도 잘 모르겠다. 


내가 영어를 못해서 못 알아 듣는 것일 수도 있고... ㅋㅋ 


자세히 하나하나의 원리를 알 고 싶다면 nomade.kr의 강의를 듣는 것을 추천.





2. 한국 대표 장고 강의 사이트 :  Nomade.kr


 진짜 장고의 '장'자도 모르는 내가 이 강의를 들으면서 조금씩 장고 웹 개발에 대한 이해를 높이고 있다. 


이 사이트 강의의 장점은 장고 웹 구조의 하나하나에 대한 자세한 설명과 이해가 있다는 부분이다. 


하지만 Udemy 사이트의 강의와는 다르게 조금 강의의 평균 시간이 약 30분이다. 


길다면 길수 있지만 다양한 것들을 많이 배울 수 있어서 좋음. (30일 구독 5만원, 60일 구독 9만원)





Udemy 사이트는 할인을 많이 하기 때문에 최저가로 사면 12,000원 정도에 강의를 볼 수 있지만, Nomade 는 유데미에 비하면 가격이 조금은 있는 편이다. 


하지만 5만원 정도에 이런 지식을 배울 수 있다는 것은... 사실 싼편이라고 생각한다. 


한국인은 역시 한국어로 된 강의를 들어야 됨...




아직 장고를 시작한지 한달이 채 되지도 않았지만 조금씩 감을 익혀가는 중이다. 


언젠간 나도 이진석 대표님 만큼 장고를 다룰 날이 오겠지.... 



노력합시다.